From 23c734d1ca5fd1ebb2ec425e63664f7efe274fe4 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:46:54 +0800 Subject: [PATCH 01/10] [vm-metadata]: data model, DTO and serialization - VmInstanceMetadataDTO, VmMetadataCategory, VmMetadataConstants - VmInstanceMetadataCodec, VmInstanceMetadataValidator - VolumeResourceMetadata, VmMetadata value object - VmMetadataDirtyVO, VmMetadataPathFingerprintVO - MetadataStorageHandler interface, VmMetadataCanonicalEvents - VmMetadataErrors error codes - RegisterVmInstanceException - Database schema V4.10.29, persistence.xml VO mapping - VmInstanceConstant/VmInstanceState metadata additions - VolumeSnapshotTree/VO_ metadata support fields Resolves: ZSV-10000 Part: 01a --- conf/db/upgrade/V4.10.29__schema.sql | 36 ++++ conf/persistence.xml | 1 + .../primary/RegisterVmInstanceException.java | 9 + .../storage/snapshot/VolumeSnapshotTree.java | 20 ++ .../storage/snapshot/VolumeSnapshotVO_.java | 4 +- .../header/vm/MetadataStorageHandler.java | 177 ++++++++++++++++++ .../zstack/header/vm/VmInstanceConstant.java | 2 + .../header/vm/VmInstanceMetadataCodec.java | 96 ++++++++++ .../vm/VmInstanceMetadataConstants.java | 82 ++++++++ .../header/vm/VmInstanceMetadataDTO.java | 141 ++++++++++++++ .../vm/VmInstanceMetadataValidator.java | 143 ++++++++++++++ .../org/zstack/header/vm/VmInstanceState.java | 4 +- .../java/org/zstack/header/vm/VmMetadata.java | 44 +++++ .../header/vm/VmMetadataCanonicalEvents.java | 31 +++ .../zstack/header/vm/VmMetadataCategory.java | 18 ++ .../zstack/header/vm/VmMetadataConstants.java | 54 ++++++ .../zstack/header/vm/VmMetadataDirtyVO.java | 143 ++++++++++++++ .../zstack/header/vm/VmMetadataDirtyVO_.java | 17 ++ .../zstack/header/vm/VmMetadataErrors.java | 28 +++ .../vm/VmMetadataPathFingerprintVO.java | 86 +++++++++ .../header/vm/VolumeResourceMetadata.java | 31 +++ 21 files changed, 1163 insertions(+), 4 deletions(-) create mode 100644 conf/db/upgrade/V4.10.29__schema.sql create mode 100644 header/src/main/java/org/zstack/header/storage/primary/RegisterVmInstanceException.java create mode 100644 header/src/main/java/org/zstack/header/vm/MetadataStorageHandler.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmInstanceMetadataCodec.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmInstanceMetadataConstants.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmInstanceMetadataDTO.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmInstanceMetadataValidator.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadata.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataCanonicalEvents.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataCategory.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataConstants.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO_.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataErrors.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataPathFingerprintVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/VolumeResourceMetadata.java diff --git a/conf/db/upgrade/V4.10.29__schema.sql b/conf/db/upgrade/V4.10.29__schema.sql new file mode 100644 index 00000000000..10aa08fc073 --- /dev/null +++ b/conf/db/upgrade/V4.10.29__schema.sql @@ -0,0 +1,36 @@ +-- Feature: VM Metadata Dirty Mark + Poller (replaces GC-based approach) + +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, + `createDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + 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; + +-- Poller CAS claim query optimization: WHERE managementNodeUuid IS NULL AND lastClaimTime ... AND nextRetryTime <= NOW() +CREATE INDEX `idxVmMetadataDirtyUnclaimed` ON `VmMetadataDirtyVO` (`managementNodeUuid`, `lastClaimTime`, `nextRetryTime`); + +-- Path fingerprint for lightweight drift detection (§8.2.3) +CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataPathFingerprintVO` ( + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `pathSnapshot` LONGTEXT, + `lastFlushTime` TIMESTAMP NULL DEFAULT NULL, + `lastFlushFailed` TINYINT(1) NOT NULL DEFAULT 0, + `staleRecoveryCount` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`vmInstanceUuid`), + CONSTRAINT `fkVmMetadataPathFingerprintVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) + REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Clean up any old GC rows for vm metadata (from previous GC-based implementation) +DELETE FROM `GarbageCollectorVO` WHERE `name` LIKE 'update-vm-%-metadata-gc'; diff --git a/conf/persistence.xml b/conf/persistence.xml index aa295bcb365..dd97b857617 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -83,6 +83,7 @@ org.zstack.header.vm.VmInstanceEO org.zstack.header.vm.VmInstanceSequenceNumberVO org.zstack.header.vm.VmCrashHistoryVO + org.zstack.header.vm.VmMetadataDirtyVO org.zstack.appliancevm.ApplianceVmVO org.zstack.appliancevm.ApplianceVmFirewallRuleVO org.zstack.header.vm.VmDnsVO diff --git a/header/src/main/java/org/zstack/header/storage/primary/RegisterVmInstanceException.java b/header/src/main/java/org/zstack/header/storage/primary/RegisterVmInstanceException.java new file mode 100644 index 00000000000..cef54c1c4e5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/RegisterVmInstanceException.java @@ -0,0 +1,9 @@ +package org.zstack.header.storage.primary; + +public interface RegisterVmInstanceException { + String updateVolumeInstallPath(String installPath); + + String updateVolumeSnapshotInstallPath(String installPath); + + PrimaryStorageType getPrimaryStorageType(); +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java index 57b4fab4099..cd6a33b4cd3 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java @@ -442,4 +442,24 @@ public SnapshotLeaf findSnapshot(String snapshotUuid) { return findSnapshot(arg -> arg.getUuid().equals(snapshotUuid)); } + + public List levelOrderTraversal() { + List result = new ArrayList<>(); + if (this.root == null) { + return result; + } + + Queue queue = new LinkedList<>(); + queue.offer(this.root); + + while (!queue.isEmpty()) { + SnapshotLeaf currentLeaf = queue.poll(); + result.add(currentLeaf.getInventory()); + for (SnapshotLeaf child : currentLeaf.getChildren()) { + queue.offer(child); + } + } + + return result; + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotVO_.java b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotVO_.java index 2d5abaf4f7f..8aeb5873f6b 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotVO_.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotVO_.java @@ -1,9 +1,7 @@ package org.zstack.header.storage.snapshot; -/** - */ - import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; @StaticMetamodel(VolumeSnapshotVO.class) public class VolumeSnapshotVO_ extends VolumeSnapshotAO_ { diff --git a/header/src/main/java/org/zstack/header/vm/MetadataStorageHandler.java b/header/src/main/java/org/zstack/header/vm/MetadataStorageHandler.java new file mode 100644 index 00000000000..4f59e965716 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/MetadataStorageHandler.java @@ -0,0 +1,177 @@ +package org.zstack.header.vm; + +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; + +import java.util.List; + +/** + * 元数据存储处理器接口 — 抽象不同存储类型的元数据读写操作。 + * + *

设计背景(Part 01c §1.3)

+ *

VM 元数据需要持久化到 VM 根盘所在的 Primary Storage 上。不同存储类型 + * (SharedBlock、Local/NFS)使用不同的存储格式和协议,本接口统一抽象 + * 这些差异,使上层逻辑(Poller、迁移流程、注册 API)无需关心底层实现。

+ * + *

Handler 动态路由(SM-07)

+ *

所有方法通过 {@code psUuid} 参数动态路由 — 每次调用时根据 + * {@code PrimaryStorageVO.type} 查找对应 Handler 实现。支持同一迁移流程中 + * 源/目标使用不同 Handler。例如 VM 从 SharedBlock 迁移到 NFS 时, + * {@code initializeMetadata(targetPsUuid)} 路由到 {@code LocalNfsMetadataStorageHandler}, + * {@code deleteMetadata(sourcePsUuid)} 路由到 {@code SblkMetadataStorageHandler}。

+ * + *

实现类

+ * + * + * + * + *
实现类存储类型
{@code SblkMetadataStorageHandler}SharedBlock
{@code LocalNfsMetadataStorageHandler}Local/NFS
+ * + *

设计约束

+ *
    + *
  • Agent 端不解析 DTO 内容;控制面负责 DTO 构建、序列化和反序列化
  • + *
  • C-01C-9: {@code deleteMetadata} 必须幂等 — 删除不存在的元数据必须返回成功
  • + *
  • C-01C-11: 必须包含 {@code scanMetadataVmUuids()} 方法
  • + *
+ * + * @see VmMetadataDirtyVO + * @see MetadataImpact + */ +public interface MetadataStorageHandler { + + /** + * 初始化 VM 元数据容器并写入完整 payload。 + * + *

VM 创建或存储迁移 Step 4 时调用。对于 sblk,创建 LV 并写入 Header + Slot A; + * 对于 local/NFS,创建 {@code .zstack-vm-metadata/} 目录(若不存在)并 + * 通过 tmp+fsync+rename 原子写入 JSON 文件。

+ * + *

若容器已存在则覆盖写入(幂等)。

+ * + * @param psUuid Primary Storage UUID — 用于路由到正确的存储后端 + * @param vmUuid VM UUID — 确定元数据文件/LV 名称 + * @param payloadJson 完整的元数据 JSON payload(由 {@code VmMetadataBuilder} 构建) + * @param completion 异步回调 + */ + void initializeMetadata(String psUuid, String vmUuid, String payloadJson, Completion completion); + + /** + * 删除 VM 元数据。 + * + *

ExpungeVm 或存储迁移 Step 7 源端清理时调用。

+ * + *

C-01C-9 幂等约束:删除不存在的元数据(LV 已删除或 JSON 文件不存在) + * 必须返回成功(不抛异常)。同时清理同名的 {@code .tmp} 和 {@code .sc.tmp} + * 残留文件(如存在),删除 tmp 失败不影响主操作成功。

+ * + * @param psUuid Primary Storage UUID + * @param vmUuid VM UUID + * @param completion 异步回调 + */ + void deleteMetadata(String psUuid, String vmUuid, Completion completion); + + /** + * 写入/更新 VM 元数据(原子操作)。 + * + *

Poller flush 时调用。sblk 使用三阶段原子写入;local/NFS 使用 + * tmp+fsync+rename 原子写入。

+ * + *

{@code storageStructureChange} 参数用于区分 tmp 文件后缀: + * {@code true} 使用 {@code .sc.tmp}(存储迁移写入), + * {@code false} 使用 {@code .tmp}(普通写入)。 + * 注册时若检测到 {@code .sc.tmp} 残留,说明存储迁移写入未完成, + * 该元数据文件标记为不可靠。

+ * + * @param psUuid Primary Storage UUID + * @param vmUuid VM UUID + * @param payloadJson 完整的元数据 JSON payload + * @param storageStructureChange 是否为存储结构变更写入(影响 tmp 文件后缀和 sblk OP type) + * @param completion 异步回调 + */ + void writeMetadata(String psUuid, String vmUuid, String payloadJson, + boolean storageStructureChange, Completion completion); + + /** + * 读取 VM 元数据。 + * + *

存储迁移 Step 5 read-back 校验、Scan/Read API 时调用。

+ * + *

返回值说明:

+ *
    + *
  • 成功读取 → {@code success(payloadJson)}
  • + *
  • 文件不存在 → {@code success(null)}
  • + *
  • 读取失败或内容损坏 → {@code fail(errorCode)}
  • + *
+ * + * @param psUuid Primary Storage UUID + * @param vmUuid VM UUID + * @param completion 异步回调,返回 JSON payload 字符串(可为 null 表示不存在) + */ + void readMetadata(String psUuid, String vmUuid, ReturnValueCompletion completion); + + /** + * 判断给定的 Primary Storage 类型是否支持元数据。 + * + *

当前支持:SharedBlock、LocalStorage、NFS。 + * 不支持的类型(ceph、zbs、vhost 等)返回 false,上层静默跳过。

+ * + * @param psType Primary Storage 类型字符串(如 "SharedBlock"、"LocalStorage"、"NFS") + * @return true 如果该存储类型支持元数据 + */ + boolean isMetadataSupported(String psType); + + /** + * 扫描指定 PS 上所有元数据条目,返回 {@link VmMetadataEntry} 列表(轻量级,不读取 payload)。 + * + *

扫描方式因存储类型而异:

+ *
    + *
  • sblk: 扫描 VG 中所有 {@code *_vmmeta} LV,提取 vmUuid 前缀
  • + *
  • local/NFS: 列举 {@code .zstack-vm-metadata/} 目录下 {@code *.json} 文件名
  • + *
+ * + *

用途:{@code MetadataOrphanDetector}(Part 2b §8.4.2)、Scan API(Part 5 §2)

+ * + *

返回类型说明(讨论 Δ-7):原方案返回 {@code List}(纯 vmUuid), + * 改为返回 {@code List}。Local Storage 场景下扫描需要逐 Host 执行, + * 调用方需要知道元数据位于哪台 Host 上以便后续操作(如孤儿清理、注册时路由)。

+ * + * @param psUuid Primary Storage UUID + * @param completion 异步回调,返回元数据条目列表 + */ + void scanMetadataVmUuids(String psUuid, ReturnValueCompletion> completion); + + /** + * 元数据扫描结果条目。 + * + *

包含 vmUuid 和可选的 hostUuid 信息。对于共享存储(SharedBlock/NFS), + * hostUuid 为 null;对于 Local Storage,hostUuid 标识元数据文件所在 Host。

+ */ + class VmMetadataEntry { + private String vmUuid; + private String hostUuid; // nullable: SharedBlock/NFS 场景为 null + + public VmMetadataEntry() { + } + + public VmMetadataEntry(String vmUuid, String hostUuid) { + this.vmUuid = vmUuid; + this.hostUuid = hostUuid; + } + + 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; + } + } +} 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..a2716386957 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -96,4 +96,6 @@ enum Capability { String VM_CDROM_OCCUPANT_ISO = "ISO"; String VM_CDROM_OCCUPANT_GUEST_TOOLS = "GuestTools"; + + String VM_META_SUFFIX = "_meta"; } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataCodec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataCodec.java new file mode 100644 index 00000000000..2153f9247cb --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataCodec.java @@ -0,0 +1,96 @@ +package org.zstack.header.vm; + +import org.zstack.utils.gson.JSONObjectUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * 虚拟机元数据编解码器。 + * + *

负责 {@link VmInstanceMetadataDTO} 与存储介质之间的编解码: + *

+ *   序列化流程:DTO → JSON String → Base64 String → byte[](写入存储)
+ *   反序列化流程:byte[](读取存储) → Base64 String → JSON String → DTO
+ * 
+ * + *

单层 Base64 编码策略:DTO 内部所有字段为明文 JSON, + * 仅在写入存储时做一次 Base64 编码。

+ */ +public class VmInstanceMetadataCodec { + + private VmInstanceMetadataCodec() { + } + + /** + * 将 DTO 编码为可写入存储的字节数组。 + * + * @param dto 元数据 DTO + * @return Base64 编码后的字节数组 + */ + public static byte[] encode(VmInstanceMetadataDTO dto) { + String json = JSONObjectUtil.toJsonString(dto); + return Base64.getEncoder().encode(json.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 将 DTO 编码为 Base64 字符串。 + * + * @param dto 元数据 DTO + * @return Base64 编码后的字符串 + */ + public static String encodeToString(VmInstanceMetadataDTO dto) { + String json = JSONObjectUtil.toJsonString(dto); + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 从存储读取的字节数组解码为 DTO。 + * + * @param data Base64 编码的字节数组 + * @return 元数据 DTO + * @throws IllegalArgumentException 如果 Base64 解码失败或 JSON 格式错误 + */ + public static VmInstanceMetadataDTO decode(byte[] data) { + byte[] jsonBytes = Base64.getDecoder().decode(data); + String json = new String(jsonBytes, StandardCharsets.UTF_8); + return JSONObjectUtil.toObject(json, VmInstanceMetadataDTO.class); + } + + /** + * 从 Base64 字符串解码为 DTO。 + * + * @param base64 Base64 编码的字符串 + * @return 元数据 DTO + * @throws IllegalArgumentException 如果 Base64 解码失败或 JSON 格式错误 + */ + public static VmInstanceMetadataDTO decodeFromString(String base64) { + byte[] jsonBytes = Base64.getDecoder().decode(base64); + String json = new String(jsonBytes, StandardCharsets.UTF_8); + return JSONObjectUtil.toObject(json, VmInstanceMetadataDTO.class); + } + + /** + * 将 DTO 序列化为 JSON 字符串(不做 Base64 编码)。 + * + *

用于调试、日志、一致性检查等场景。

+ * + * @param dto 元数据 DTO + * @return JSON 字符串 + */ + public static String toJson(VmInstanceMetadataDTO dto) { + return JSONObjectUtil.toJsonString(dto); + } + + /** + * 从 JSON 字符串反序列化为 DTO(不做 Base64 解码)。 + * + *

用于调试、测试等场景。

+ * + * @param json JSON 字符串 + * @return 元数据 DTO + */ + public static VmInstanceMetadataDTO fromJson(String json) { + return JSONObjectUtil.toObject(json, VmInstanceMetadataDTO.class); + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataConstants.java b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataConstants.java new file mode 100644 index 00000000000..ec9b5a231ee --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataConstants.java @@ -0,0 +1,82 @@ +package org.zstack.header.vm; + +/** + * 虚拟机元数据相关常量。 + */ +public class VmInstanceMetadataConstants { + + private VmInstanceMetadataConstants() { + } + + /** + * 元数据 LV 后缀(sblk 场景)。 + * + *

LV 命名规则:{vm_uuid}_vmmeta

+ */ + public static final String SBLK_LV_SUFFIX = "_vmmeta"; + + /** + * 元数据文件名(local/NFS 场景)。 + * + *

文件位于与根盘同目录下。

+ */ + public static final String METADATA_FILE_NAME = "vm_metadata.json"; + + /** + * sblk 元数据 LV 默认初始大小(字节):4MB。 + */ + public static final long SBLK_LV_INITIAL_SIZE = 4L * 1024 * 1024; + + /** + * sblk 元数据 LV 最大大小(字节):64MB。 + */ + public static final long SBLK_LV_MAX_SIZE = 64L * 1024 * 1024; + + /** + * sblk 写入序列号最大值。溢出后回绕到 1。 + */ + public static final long MAX_WRITE_SEQUENCE = 0xFFFFFFFFFFFFFFFFL; + + /** + * 全局配置:是否启用虚拟机元数据记录。 + * + *

默认关闭。开启后,API 操作成功时自动触发元数据更新。

+ */ + public static final String GLOBAL_CONFIG_METADATA_ENABLED = "vm.metadata.enabled"; + + /** + * GC 初始延迟秒数。 + * + *

API 成功后延迟该秒数再触发元数据更新, + * 避免短时间内多次 API 操作产生过多无用更新。

+ */ + public static final int INITIAL_GC_DELAY_SECONDS = 5; + + /** + * 注册虚拟机 MN 标识 System Tag 前缀。 + * + *

注册过程中在 VM 上打标记,记录执行注册的 MN UUID, + * 用于 MN 崩溃后的事务回滚判断。

+ */ + public static final String REGISTERING_MN_TAG_PREFIX = "vmMetadata::registeringMnUuid::"; + + /** + * VM 状态:注册中。 + * + *

注册开始时 VM 进入此中间状态,注册完成后转为 Stopped。

+ */ + public static final String VM_STATE_REGISTERING = "Registering"; + + /** + * ChainTask 最大排队任务数。 + * + *

同一 VM 的元数据更新 ChainTask 最多排队 1 个, + * 超出的通过 exceedMaxPendingCallback 立即 Done。

+ */ + public static final int MAX_PENDING_METADATA_TASKS = 1; + + /** + * ChainTask syncSignature 前缀。 + */ + public static final String CHAIN_TASK_SIGNATURE_PREFIX = "vm-metadata-update-"; +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataDTO.java b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataDTO.java new file mode 100644 index 00000000000..d0a3ee2db8f --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataDTO.java @@ -0,0 +1,141 @@ +package org.zstack.header.vm; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * 虚拟机元数据 DTO。 + * + *

存储在主存储上的元数据文件内容就是该 DTO 的 JSON 字符串经 Base64 编码后的结果。

+ * + *

编码策略

+ *

DTO 内部所有字段均为明文 JSON。由存储写入层对整个 DTO 的 JSON 字符串做一次统一 + * Base64 编码后写入存储介质(sblk Slot Payload / local NFS 文件内容)。

+ * + *

Checksum

+ *

Checksum 不作为 DTO 字段,由存储层保证: + *

    + *
  • sblk: Slot 结构自带 Checksum 字段
  • + *
  • local/NFS: tmp + rename 原子写入保证完整性
  • + *
+ */ +public class VmInstanceMetadataDTO { + + /** + * 资源元数据子结构。 + * + *

对于每种资源(VM、Volume、Nic),记录其 VO 全量 JSON 及关联的 SystemTag/ResourceConfig。

+ */ + public static class ResourceMetadata { + /** + * 资源 UUID。 + * + *

冗余字段,反序列化时必须校验与 {@link #vo} 内部的 uuid 字段一致。

+ */ + @SerializedName("resourceUuid") + public String resourceUuid; + + /** + * VO 全量 JSON 明文。 + * + *
    + *
  • {@link VmInstanceMetadataDTO#vm} → VmInstanceVO JSON
  • + *
  • {@link VmInstanceMetadataDTO#volumes} 元素 → VolumeVO JSON
  • + *
  • {@link VmInstanceMetadataDTO#nics} 元素 → VmNicVO JSON
  • + *
+ * + *

序列化时由 Gson 自动处理嵌套 JSON 的转义;反序列化时需要二次反序列化为具体 VO 类。

+ */ + @SerializedName("vo") + public String vo; + + /** + * SystemTag 列表的 Base64 编码。 + * + *

构建过程:SystemTagVO 列表 → 逐个 JSON 序列化 → 组成 JSON Array 字符串 → Base64 编码。 + * Base64 编码是为了保护可能包含的密码、密钥等敏感信息。

+ */ + @SerializedName("systemTags") + public String systemTags; + + /** + * ResourceConfig 列表的 Base64 编码。 + * + *

构建过程与 systemTags 一致。

+ */ + @SerializedName("resourceConfigs") + public String resourceConfigs; + } + + /** + * 元数据 schema 版本,与 ZStack 数据库版本(zsv)一致,如 "5.0.0"。 + * + *

序列化时自动填充当前平台版本。注册时若版本不匹配则拒绝注册。 + * 升级后通过全量更新 GC 将所有 VM 的元数据刷新到新版本。

+ */ + @SerializedName("schemaVersion") + public String schemaVersion; + + /** + * 虚拟机分类。 + * + *

标识本元数据所属 VM 的分类(普通 / 模板 / 模板缓存), + * 注册恢复时按不同分类执行不同的恢复逻辑。

+ */ + @SerializedName("vmCategory") + public VmMetadataCategory vmCategory; + + /** + * 虚拟机自身的元数据。 + * + *

{@link ResourceMetadata#vo} 为 VmInstanceVO 的 JSON。

+ */ + @SerializedName("vm") + public ResourceMetadata vm; + + /** + * 云盘元数据列表。 + * + *

包含根盘与数据盘(挂载的 + 已卸载但 lastVmInstanceUuid 指向本 VM 的)。 + * 不包含共享盘(isShareable=true 的 Volume 被排除)。 + * {@link VolumeResourceMetadata#vo} 为 VolumeVO 的 JSON, + * 每个 Volume 的快照引用数据内嵌在 {@link VolumeResourceMetadata} 中。

+ */ + @SerializedName("volumes") + public List volumes; + + /** + * 网卡元数据列表。 + * + *

仅记录,注册时不恢复。{@link ResourceMetadata#vo} 为 VmNicVO 的 JSON。

+ */ + @SerializedName("nics") + public List nics; + + /** + * 快照数据(扁平列表)。 + * + *

所有 Volume 下的 VolumeSnapshotVO JSON 明文的扁平列表, + * 按 BFS 拓扑序排列(父快照在子快照之前)。

+ */ + @SerializedName("snapshots") + public List snapshots; + + /** + * 快照组列表。 + * + *

每个元素是 VolumeSnapshotGroupVO 的 JSON 明文。

+ */ + @SerializedName("snapshotGroups") + public List snapshotGroups; + + /** + * 快照组关联引用列表。 + * + *

每个元素是 VolumeSnapshotGroupRefVO 的 JSON 明文。 + * 通过 {@code volumeSnapshotGroupUuid} 字段与 {@link #snapshotGroups} 关联。

+ */ + @SerializedName("snapshotGroupRefs") + public List snapshotGroupRefs; +} diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataValidator.java b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataValidator.java new file mode 100644 index 00000000000..2a9722511ad --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataValidator.java @@ -0,0 +1,143 @@ +package org.zstack.header.vm; + +import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.utils.gson.JSONObjectUtil; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 虚拟机元数据校验器。 + * + *

在反序列化后、注册前执行校验,确保元数据完整性和一致性。

+ * + *

校验项: + *

    + *
  • schemaVersion 与当前平台版本匹配
  • + *
  • ResourceMetadata.resourceUuid 与 vo 内部 uuid 一致
  • + *
  • snapshotGroupRefs 引用的 groupUuid 必须存在于 snapshotGroups 中
  • + *
+ */ +public class VmInstanceMetadataValidator { + + private VmInstanceMetadataValidator() { + } + + /** + * 执行全量校验。 + * + * @param dto 待校验的元数据 DTO + * @param currentVersion 当前平台 schema 版本 + * @throws CloudRuntimeException 校验失败时抛出 + */ + public static void validate(VmInstanceMetadataDTO dto, String currentVersion) { + validateSchemaVersion(dto, currentVersion); + validateResourceUuidConsistency(dto); + validateSnapshotGroupIntegrity(dto); + } + + /** + * 校验 schema 版本是否匹配当前平台版本。 + * + * @param dto 待校验的元数据 DTO + * @param currentVersion 当前平台 schema 版本 + * @throws CloudRuntimeException 版本缺失或不匹配时抛出 + */ + public static void validateSchemaVersion(VmInstanceMetadataDTO dto, String currentVersion) { + if (dto.schemaVersion == null || dto.schemaVersion.isEmpty()) { + throw new CloudRuntimeException("metadata schemaVersion is missing"); + } + if (!dto.schemaVersion.equals(currentVersion)) { + throw new CloudRuntimeException(String.format( + "metadata schemaVersion[%s] does not match current platform version[%s]," + + " please upgrade metadata first", + dto.schemaVersion, currentVersion)); + } + } + + /** + * 校验所有 ResourceMetadata 的 resourceUuid 与 vo 内部 uuid 一致。 + * + * @param dto 待校验的元数据 DTO + * @throws CloudRuntimeException resourceUuid 缺失或与 vo.uuid 不一致时抛出 + */ + public static void validateResourceUuidConsistency(VmInstanceMetadataDTO dto) { + if (dto.vm != null) { + validateSingleResourceUuid(dto.vm, "vm"); + } + if (dto.volumes != null) { + for (int i = 0; i < dto.volumes.size(); i++) { + validateSingleResourceUuid(dto.volumes.get(i), "volumes[" + i + "]"); + } + } + if (dto.nics != null) { + for (int i = 0; i < dto.nics.size(); i++) { + validateSingleResourceUuid(dto.nics.get(i), "nics[" + i + "]"); + } + } + } + + @SuppressWarnings("unchecked") + private static void validateSingleResourceUuid(VmInstanceMetadataDTO.ResourceMetadata rm, String path) { + if (rm.resourceUuid == null) { + throw new CloudRuntimeException(String.format( + "metadata %s.resourceUuid is null", path)); + } + if (rm.vo == null) { + throw new CloudRuntimeException(String.format( + "metadata %s.vo is null", path)); + } + + Map voMap = JSONObjectUtil.toObject(rm.vo, Map.class); + Object voUuid = voMap.get("uuid"); + if (voUuid == null) { + throw new CloudRuntimeException(String.format( + "metadata %s.vo does not contain uuid field", path)); + } + if (!rm.resourceUuid.equals(voUuid.toString())) { + throw new CloudRuntimeException(String.format( + "metadata %s.resourceUuid[%s] does not match vo.uuid[%s]", + path, rm.resourceUuid, voUuid)); + } + } + + /** + * 校验快照组引用的完整性。 + * + *

snapshotGroupRefs 中引用的 volumeSnapshotGroupUuid + * 必须存在于 snapshotGroups 中。

+ * + * @param dto 待校验的元数据 DTO + * @throws CloudRuntimeException 引用了不存在的 group 时抛出 + */ + @SuppressWarnings("unchecked") + public static void validateSnapshotGroupIntegrity(VmInstanceMetadataDTO dto) { + if (dto.snapshotGroupRefs == null || dto.snapshotGroupRefs.isEmpty()) { + return; + } + if (dto.snapshotGroups == null || dto.snapshotGroups.isEmpty()) { + throw new CloudRuntimeException( + "metadata has snapshotGroupRefs but no snapshotGroups"); + } + + Set groupUuids = new HashSet<>(); + for (String groupJson : dto.snapshotGroups) { + Map groupMap = JSONObjectUtil.toObject(groupJson, Map.class); + Object uuid = groupMap.get("uuid"); + if (uuid != null) { + groupUuids.add(uuid.toString()); + } + } + + for (String refJson : dto.snapshotGroupRefs) { + Map refMap = JSONObjectUtil.toObject(refJson, Map.class); + Object groupUuid = refMap.get("volumeSnapshotGroupUuid"); + if (groupUuid != null && !groupUuids.contains(groupUuid.toString())) { + throw new CloudRuntimeException(String.format( + "metadata snapshotGroupRef references non-existent group[uuid:%s]", + groupUuid)); + } + } + } +} \ 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..1e5a9947ec9 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), diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadata.java b/header/src/main/java/org/zstack/header/vm/VmMetadata.java new file mode 100644 index 00000000000..92452753804 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadata.java @@ -0,0 +1,44 @@ +package org.zstack.header.vm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VmMetadata { + public String vmInstanceVO; + public List vmSystemTags = new ArrayList<>(); + public List vmResourceConfigs = new ArrayList<>(); + + public List volumeVOs = new ArrayList<>(); + // key = volumeUuid + // value = SystemTag + public Map> volumeSystemTags = new HashMap<>(); + // key = volumeUuid + // value = ResourceConfig + public Map> volumeResourceConfigs = new HashMap<>(); + + public List vmNicVOs = new ArrayList<>(); + // key = nicUuid + // value = SystemTag + public Map> vmNicSystemTags = new HashMap<>(); + // key = nicUuid + // value = ResourceConfig + public Map> vmNicResourceConfigs = new HashMap<>(); + + // key = volumeUuid + // value = List + public Map> volumeSnapshots = new HashMap<>(); + + // VolumeSnapshotGroupVO.toString + public List volumeSnapshotGroupVO = new ArrayList<>(); + // VolumeSnapshotGroupRefVO.toString + public List volumeSnapshotGroupRefVO = new ArrayList<>(); + + // key = volumeUuid + // value = VolumeSnapshotReferenceVO.toString + public Map volumeSnapshotReferenceVO = new HashMap<>(); + // key = volumeUuid + // value = VolumeSnapshotReferenceTreeVO.toString + public Map volumeSnapshotReferenceTreeVO = new HashMap<>(); +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataCanonicalEvents.java b/header/src/main/java/org/zstack/header/vm/VmMetadataCanonicalEvents.java new file mode 100644 index 00000000000..dd5323fea7e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataCanonicalEvents.java @@ -0,0 +1,31 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedJsonSchema; + +/** + * 虚拟机元数据相关 CanonicalEvent 定义。 + * + *

通过 {@code EventFacade.fire()} 发布,供监控系统和巡检机制消费。

+ */ +public class VmMetadataCanonicalEvents { + + /** + * GC 放弃后的 stale 事件路径。 + * + *

当 {@code UpdateVmInstanceMetadataGC} 超过最大重试次数后发布此事件, + * {@code MetadataHealthCheckJob} 监听此事件将 VM 加入优先刷新队列。

+ */ + public static final String VM_METADATA_STALE_PATH = "/vm/metadata/stale"; + + @NeedJsonSchema + public static class MetadataStaleData { + public String vmInstanceUuid; + + public MetadataStaleData() { + } + + public MetadataStaleData(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + } +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataCategory.java b/header/src/main/java/org/zstack/header/vm/VmMetadataCategory.java new file mode 100644 index 00000000000..8945760d3e5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataCategory.java @@ -0,0 +1,18 @@ +package org.zstack.header.vm; + +/** + * 虚拟机元数据分类。 + * + *

用于区分元数据所属的 VM 类型,注册恢复时按不同分类执行不同的恢复逻辑。

+ * + *
    + *
  • {@link #REGULAR} — 普通云主机
  • + *
  • {@link #TEMPLATE} — 模板虚拟机
  • + *
  • {@link #TEMPLATE_CACHE} — 模板虚拟机缓存
  • + *
+ */ +public enum VmMetadataCategory { + REGULAR, + TEMPLATE, + TEMPLATE_CACHE +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataConstants.java b/header/src/main/java/org/zstack/header/vm/VmMetadataConstants.java new file mode 100644 index 00000000000..d5f26e11427 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataConstants.java @@ -0,0 +1,54 @@ +package org.zstack.header.vm; + +/** + * SharedBlock 元数据存储的容量常量与 Payload 大小保护阈值。 + * + *

SharedBlock(sblk)使用固定大小的 LV 存储 VM 元数据,采用双 Slot 布局: + *

+ *   [ LV Header (4096B) ][ Slot-A ][ Slot-B ]
+ *   Slot 大小 = (lvSize - headerSize) / 2,向下对齐到 4096
+ *   Slot Header = 36B(Magic 4B + SeqNum 8B + SlotOffset 8B + SlotCapacity 8B + PayloadLen 8B)
+ *   可用 Payload = SlotCapacity - SlotHeaderSize
+ * 
+ * + * @see Part 02b §10.0 容量公式与常量 + */ +public final class VmMetadataConstants { + + private VmMetadataConstants() { + // utility class + } + + /** LV 头部大小(字节) */ + public static final long SBLK_HEADER_SIZE = 4096L; + + /** Slot 头部大小(字节):Magic(4) + SeqNum(8) + SlotOffset(8) + SlotCapacity(8) + PayloadLen(8) */ + public static final long SBLK_SLOT_HEADER_SIZE = 36L; + + /** SharedBlock 元数据 LV 最大大小(64MB) */ + public static final long SBLK_MAX_LV_SIZE = 64L * 1024 * 1024; + + /** + * 计算给定 LV 大小下单个 Slot 的容量(字节)。 + * + *

公式:((lvSize - headerSize) / 2 / 4096) * 4096(向下对齐到 4096)

+ * + * @param lvSize LV 总大小(字节) + * @return 单个 Slot 的容量(字节) + */ + public static long slotCapacity(long lvSize) { + return ((lvSize - SBLK_HEADER_SIZE) / 2 / 4096) * 4096; + } + + /** 64MB LV 下单个 Slot 的最大容量(约 33,550,336 字节) */ + public static final long SBLK_MAX_SLOT_CAPACITY = slotCapacity(SBLK_MAX_LV_SIZE); + + /** 64MB LV 下单个 Slot 的最大可用 Payload(约 33,550,300 字节) */ + public static final long SBLK_MAX_PAYLOAD_SIZE = SBLK_MAX_SLOT_CAPACITY - SBLK_SLOT_HEADER_SIZE; + + /** Payload 大小预警阈值(8MB):超过时输出 WARN 日志 */ + public static final long PAYLOAD_WARN_THRESHOLD = 8L * 1024 * 1024; + + /** Payload 大小拒绝阈值(30MB):超过时 ERROR + 拒绝写入 */ + public static final long PAYLOAD_REJECT_THRESHOLD = 30L * 1024 * 1024; +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO.java b/header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO.java new file mode 100644 index 00000000000..60bf5231263 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO.java @@ -0,0 +1,143 @@ +package org.zstack.header.vm; + +import org.zstack.header.managementnode.ManagementNodeVO; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ForeignKey.ReferenceOption; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * 记录 VM 元数据的"脏标记",表示该 VM 的元数据需要写入主存储。 + * + *

设计要点

+ *
    + *
  • vmInstanceUuid 做主键:一个 VM 最多一行,天然去重。 + * 100 个 API 只产生 1 行,不是 100 行。
  • + *
  • managementNodeUuid FK SET_NULL:MN 宕机后 DB 约束自动释放认领, + * 无需额外孤儿扫描。
  • + *
  • vmInstanceUuid FK CASCADE:VM 销毁时自动删除脏标记,无残留。
  • + *
  • dirtyVersion:每次 markDirty +1,刷写前快照 version, + * 成功后比较——检测刷写期间是否有新变更。语义比时间戳比较更明确,无精度问题。
  • + *
  • nextRetryTime:退避控制,失败后不立刻重试,等到下次重试时间。
  • + *
+ * + *

行语义

+ *
    + *
  • 行存在 = VM 元数据是脏的(需要刷写)
  • + *
  • 行不存在 = VM 元数据已是最新(或 VM 不存在)
  • + *
  • managementNodeUuid != null = 该行已被某个 MN 认领,正在处理
  • + *
  • managementNodeUuid == null = 该行未被认领,可被 Poller 或 triggerFlush 认领
  • + *
+ */ +@Entity +@Table(name = "VmMetadataDirtyVO") +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; + + @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/VmMetadataDirtyVO_.java b/header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO_.java new file mode 100644 index 00000000000..8ed099d9d8e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataDirtyVO_.java @@ -0,0 +1,17 @@ +package org.zstack.header.vm; + +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 createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataErrors.java b/header/src/main/java/org/zstack/header/vm/VmMetadataErrors.java new file mode 100644 index 00000000000..8e37a63dc67 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataErrors.java @@ -0,0 +1,28 @@ +package org.zstack.header.vm; + +public enum VmMetadataErrors { + METADATA_INVALID_FORMAT(1300), + METADATA_SCHEMA_VERSION_MISMATCH(1301), + METADATA_UUID_CONFLICT(1302), + METADATA_STORAGE_NOT_SUPPORTED(1303), + METADATA_CROSS_STORAGE_FORBIDDEN(1304), + METADATA_INSTALL_PATH_NOT_FOUND(1305), + METADATA_CACHE_VM_NOT_REGISTERABLE(1306), + METADATA_VM_REGISTERING(1307), + METADATA_READ_CORRUPTED(1308), + METADATA_PAYLOAD_TOO_LARGE(1309), + METADATA_PS_UNREACHABLE(1310), + METADATA_FEATURE_DISABLED(1311), + ; + + private String code; + + private VmMetadataErrors(int id) { + code = String.format("VM_METADATA.%s", id); + } + + @Override + public String toString() { + return code; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataPathFingerprintVO.java b/header/src/main/java/org/zstack/header/vm/VmMetadataPathFingerprintVO.java new file mode 100644 index 00000000000..62268413d79 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataPathFingerprintVO.java @@ -0,0 +1,86 @@ +package org.zstack.header.vm; + +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ForeignKey.ReferenceOption; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * 路径指纹:记录每个 VM 上次成功刷写元数据时的存储拓扑路径快照。 + * + *

设计要点(Part 02b §8.2.3)

+ *
    + *
  • pathSnapshot:JSON 格式的 volumes/snapshots installPath 列表, + * 按 uuid ASC 排序保证确定性,用于纯 DB 侧路径漂移检测(零存储 I/O)。
  • + *
  • lastFlushFailed:Poller 重试耗尽时置 true(C-SR-05), + * 仅由 {@code MetadataStaleRecoveryTask} 重置为 false(C-02B-8)。
  • + *
  • staleRecoveryCount:熔断计数器,{@code MetadataStaleRecoveryTask} 每次 + * 重入队递增,达到上限(默认 10 ≈ 5 小时)后停止自动恢复。 + * 管理员可通过 {@code APIUpdateVmMetadataMsg} 手动重置为 0。
  • + *
  • vmInstanceUuid 做 PK:一个 VM 最多一行。 + * FK CASCADE 保证 VM 物理删除时自动清理。
  • + *
+ */ +@Entity +@Table(name = "VmMetadataPathFingerprintVO") +public class VmMetadataPathFingerprintVO { + + @Id + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ReferenceOption.CASCADE) + private String vmInstanceUuid; + + @Column + @Lob + private String pathSnapshot; + + @Column + private Timestamp lastFlushTime; + + @Column + private boolean lastFlushFailed; + + @Column + private int staleRecoveryCount; + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getPathSnapshot() { + return pathSnapshot; + } + + public void setPathSnapshot(String pathSnapshot) { + this.pathSnapshot = pathSnapshot; + } + + public Timestamp getLastFlushTime() { + return lastFlushTime; + } + + public void setLastFlushTime(Timestamp lastFlushTime) { + this.lastFlushTime = lastFlushTime; + } + + public boolean isLastFlushFailed() { + return lastFlushFailed; + } + + public void setLastFlushFailed(boolean lastFlushFailed) { + this.lastFlushFailed = lastFlushFailed; + } + + public int getStaleRecoveryCount() { + return staleRecoveryCount; + } + + public void setStaleRecoveryCount(int staleRecoveryCount) { + this.staleRecoveryCount = staleRecoveryCount; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/VolumeResourceMetadata.java b/header/src/main/java/org/zstack/header/vm/VolumeResourceMetadata.java new file mode 100644 index 00000000000..cff9943392a --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VolumeResourceMetadata.java @@ -0,0 +1,31 @@ +package org.zstack.header.vm; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * 云盘资源元数据,扩展 {@link VmInstanceMetadataDTO.ResourceMetadata} 以包含 + * 快照引用(VolumeSnapshotReferenceVO)和快照引用树(VolumeSnapshotReferenceTreeVO)数据。 + * + *

每个 Volume 的快照引用数据直接关联到对应的 VolumeResourceMetadata 中, + * 而非放在 DTO 顶层的 Map 结构里,便于按卷维度整体操作。

+ */ +public class VolumeResourceMetadata extends VmInstanceMetadataDTO.ResourceMetadata { + /** + * 该 Volume 关联的快照引用列表。 + * + *

每个元素是 VolumeSnapshotReferenceVO 的 JSON 明文。 + * 通过 {@code referenceVolumeUuid} 查询关联到本 Volume。

+ */ + @SerializedName("snapshotReferences") + public List snapshotReferences; + + /** + * 该 Volume 关联的快照引用树列表。 + * + *

每个元素是 VolumeSnapshotReferenceTreeVO 的 JSON 明文。

+ */ + @SerializedName("snapshotReferenceTrees") + public List snapshotReferenceTrees; +} From fc7d8da7d3a9b94f513bb8aadf11032431568001 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:47:31 +0800 Subject: [PATCH 02/10] [vm-metadata]: API definitions and message classes - Scan/Read/Register/Check/Update/PreCheck/Cleanup public APIs - ConvertTemplatedVm/ConvertToTemplatedVm API messages - Internal messages: UpdateVmInstanceMetadata, ReadVmInstanceMetadata - Internal messages: GetVmInstanceMetadataFromPrimaryStorage - Internal messages: CleanupVmInstanceMetadataOnPrimaryStorage - RegisterVmFromMetadataInner, BatchCheckMetadataStatus, RepairMetadata - Result DTOs: VmMetadataScanResult, ConsistencyCheckResult, DiffEntry - MetadataStatusResult, PreCheckItem, VmInstanceMetadataRegistrationSpec - SDK: RegisterVmInstanceAction/Result - PrimaryStorage-level API: Get/RegisterVmInstance Resolves: ZSV-10000 Part: 05 --- ...InstanceMetadataFromPrimaryStorageMsg.java | 36 ++++++ ...stanceMetadataFromPrimaryStorageReply.java | 26 ++++ ...APIRegisterVmInstanceEventDoc_zh_cn.groovy | 32 +++++ .../primary/APIRegisterVmInstanceMsg.java | 63 ++++++++++ .../APIRegisterVmInstanceMsgDoc_zh_cn.groovy | 83 +++++++++++++ .../primary/APIRegisterVmInstanceReply.java | 103 ++++++++++++++++ ...VmInstanceMetadataOnPrimaryStorageMsg.java | 27 +++++ ...InstanceMetadataOnPrimaryStorageReply.java | 36 ++++++ ...InstanceMetadataFromPrimaryStorageMsg.java | 17 +++ ...stanceMetadataFromPrimaryStorageReply.java | 20 ++++ .../primary/ReadVmInstanceMetadataMsg.java | 21 ++++ ...ReadVmInstanceMetadataOnHypervisorMsg.java | 26 ++++ ...adVmInstanceMetadataOnHypervisorReply.java | 15 +++ .../primary/ReadVmInstanceMetadataReply.java | 15 +++ ...CheckVmInstanceMetadataConsistencyMsg.java | 56 +++++++++ ...eckVmInstanceMetadataConsistencyReply.java | 31 +++++ .../vm/APICleanupVmInstanceMetadataEvent.java | 53 ++++++++ .../vm/APICleanupVmInstanceMetadataMsg.java | 44 +++++++ ...ertTemplatedVmInstanceToVmInstanceMsg.java | 1 + ...ertVmInstanceToTemplatedVmInstanceMsg.java | 1 + .../APIPreCheckVmMetadataRegistrationMsg.java | 79 ++++++++++++ ...PIPreCheckVmMetadataRegistrationReply.java | 30 +++++ .../vm/APIReadVmInstanceMetadataMsg.java | 48 ++++++++ .../vm/APIReadVmInstanceMetadataReply.java | 65 ++++++++++ ...PIRegisterVmInstanceFromMetadataEvent.java | 45 +++++++ .../APIRegisterVmInstanceFromMetadataMsg.java | 88 ++++++++++++++ .../vm/APIScanVmInstanceMetadataMsg.java | 45 +++++++ .../vm/APIScanVmInstanceMetadataReply.java | 35 ++++++ .../header/vm/APIUpdateVmMetadataEvent.java | 19 +++ .../header/vm/APIUpdateVmMetadataMsg.java | 36 ++++++ .../vm/BatchCheckMetadataStatusMsg.java | 39 ++++++ .../vm/BatchCheckMetadataStatusReply.java | 26 ++++ .../header/vm/ConsistencyCheckResult.java | 43 +++++++ .../java/org/zstack/header/vm/DiffEntry.java | 33 +++++ .../header/vm/MetadataStatusResult.java | 65 ++++++++++ .../org/zstack/header/vm/PreCheckItem.java | 33 +++++ .../vm/RegisterVmFromMetadataInnerMsg.java | 64 ++++++++++ .../vm/RegisterVmFromMetadataInnerReply.java | 26 ++++ .../zstack/header/vm/RepairMetadataMsg.java | 52 ++++++++ .../zstack/header/vm/RepairMetadataReply.java | 11 ++ .../vm/UpdateVmInstanceMetadataMsg.java | 43 +++++++ ...dateVmInstanceMetadataOnHypervisorMsg.java | 86 +++++++++++++ ...teVmInstanceMetadataOnHypervisorReply.java | 9 ++ ...VmInstanceMetadataOnPrimaryStorageMsg.java | 84 +++++++++++++ ...InstanceMetadataOnPrimaryStorageReply.java | 9 ++ .../vm/UpdateVmInstanceMetadataReply.java | 9 ++ .../VmInstanceMetadataRegistrationSpec.java | 92 ++++++++++++++ .../header/vm/VmMetadataScanResult.java | 87 ++++++++++++++ .../zstack/sdk/RegisterVmInstanceAction.java | 113 ++++++++++++++++++ .../zstack/sdk/RegisterVmInstanceResult.java | 14 +++ 50 files changed, 2134 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceReply.java 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/ReadVmInstanceMetadataOnHypervisorMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorReply.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/vm/APICheckVmInstanceMetadataConsistencyMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/ConsistencyCheckResult.java create mode 100644 header/src/main/java/org/zstack/header/vm/DiffEntry.java create mode 100644 header/src/main/java/org/zstack/header/vm/MetadataStatusResult.java create mode 100644 header/src/main/java/org/zstack/header/vm/PreCheckItem.java create mode 100644 header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/RepairMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/RepairMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmInstanceMetadataRegistrationSpec.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmMetadataScanResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceResult.java diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java new file mode 100644 index 00000000000..aab3976a182 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,36 @@ +package org.zstack.header.storage.primary; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; + + +@RestRequest( + path = "/primary-storage/vm-instances/metadata", + method = HttpMethod.GET, + responseClass = APIGetVmInstanceMetadataFromPrimaryStorageReply.class +) +public class APIGetVmInstanceMetadataFromPrimaryStorageMsg extends APISyncCallMessage 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 APIGetVmInstanceMetadataFromPrimaryStorageMsg __example__() { + APIGetVmInstanceMetadataFromPrimaryStorageMsg msg = new APIGetVmInstanceMetadataFromPrimaryStorageMsg(); + msg.setUuid(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageReply.java new file mode 100644 index 00000000000..5e2dfa123fe --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageReply.java @@ -0,0 +1,26 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.ArrayList; +import java.util.List; + + +@RestResponse(allTo = "all") +public class APIGetVmInstanceMetadataFromPrimaryStorageReply extends APIReply { + private List vmInstanceMetadata = new ArrayList<>(); + + public List getVmInstanceMetadata() { + return vmInstanceMetadata; + } + + public void setVmInstanceMetadata(List vmInstanceMetadata) { + this.vmInstanceMetadata = vmInstanceMetadata; + } + + public static APIGetVmInstanceMetadataFromPrimaryStorageReply __example__() { + APIGetVmInstanceMetadataFromPrimaryStorageReply reply = new APIGetVmInstanceMetadataFromPrimaryStorageReply(); + return reply; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..972cbf23154 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.vm.VmInstanceInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "注册虚拟机返回" + + ref { + name "inventory" + path "org.zstack.header.storage.primary.APIRegisterVmInstanceEvent.inventory" + desc "null" + type "VmInstanceInventory" + since "4.10.0" + clz VmInstanceInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "4.10.0" + } + ref { + name "error" + path "org.zstack.header.storage.primary.APIRegisterVmInstanceEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "4.10.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java new file mode 100644 index 00000000000..4b2c4e80778 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java @@ -0,0 +1,63 @@ +package org.zstack.header.storage.primary; + +import org.springframework.http.HttpMethod; +import org.zstack.header.cluster.ClusterVO; +import org.zstack.header.host.HostVO; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +@RestRequest( + path = "/vm-instances/register", + method = HttpMethod.POST, + responseClass = APIRegisterVmInstanceReply.class, + parameterName = "params" +) +public class APIRegisterVmInstanceMsg extends APIMessage implements PrimaryStorageMessage { + @APIParam() + private String metadataPath; + @APIParam(resourceType = PrimaryStorageVO.class) + private String primaryStorageUuid; + @APIParam(resourceType = ClusterVO.class) + private String clusterUuid; + @APIParam(required = false, resourceType = HostVO.class) + private String hostUuid; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + 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 getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public static APIRegisterVmInstanceMsg __example__() { + APIRegisterVmInstanceMsg msg = new APIRegisterVmInstanceMsg(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..9772948e4dd --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsgDoc_zh_cn.groovy @@ -0,0 +1,83 @@ +package org.zstack.header.storage.primary + +doc { + title "RegisterVmInstance" + + category "storage.primary" + + desc """注册虚拟机""" + + rest { + request { + url "POST /v1/vm-instances/register" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIRegisterVmInstanceMsg.class + + desc """""" + + params { + + column { + name "primaryStorageUuid" + enclosedIn "params" + desc "主存储UUID" + location "body" + type "String" + optional false + since "4.10.0" + } + column { + name "clusterUuid" + enclosedIn "params" + desc "集群UUID" + location "body" + type "String" + optional false + since "4.10.0" + } + column { + name "hostUuid" + enclosedIn "params" + desc "物理机UUID" + location "body" + type "String" + optional true + since "4.10.0" + } + column { + name "metadataPath" + enclosedIn "params" + desc "" + location "body" + type "String" + optional false + since "4.10.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "4.10.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "4.10.0" + } + } + } + + response { + clz APIRegisterVmInstanceReply.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceReply.java b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceReply.java new file mode 100644 index 00000000000..eb5c1fdda69 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceReply.java @@ -0,0 +1,103 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.allocator.HostAllocatorConstant; +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceState; +import org.zstack.header.vm.VmNicInventory; +import org.zstack.header.volume.VolumeInventory; +import org.zstack.header.volume.VolumeState; +import org.zstack.header.volume.VolumeStatus; +import org.zstack.header.volume.VolumeType; +import org.zstack.utils.data.SizeUnit; + +import java.sql.Timestamp; + +import static java.util.Arrays.asList; + +@RestResponse(allTo = "inventory") +public class APIRegisterVmInstanceReply extends APIEvent { + private VmInstanceInventory inventory; + + public APIRegisterVmInstanceReply() { + } + + public APIRegisterVmInstanceReply(String apiId) { + super(apiId); + } + + public VmInstanceInventory getInventory() { + return inventory; + } + + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + + public static APIRegisterVmInstanceReply __example__() { + APIRegisterVmInstanceReply event = new APIRegisterVmInstanceReply(); + + + String defaultL3Uuid = uuid(); + String rootVolumeUuid = uuid(); + + VmInstanceInventory vm = new VmInstanceInventory(); + vm.setName("Test-VM"); + vm.setUuid(uuid()); + vm.setAllocatorStrategy(HostAllocatorConstant.LAST_HOST_PREFERRED_ALLOCATOR_STRATEGY_TYPE); + vm.setClusterUuid(uuid()); + vm.setCpuNum(1); + vm.setCreateDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + vm.setDefaultL3NetworkUuid(defaultL3Uuid); + vm.setDescription("web server VM"); + vm.setHostUuid(uuid()); + vm.setHypervisorType("KVM"); + vm.setImageUuid(uuid()); + vm.setInstanceOfferingUuid(uuid()); + vm.setLastHostUuid(uuid()); + vm.setMemorySize(SizeUnit.GIGABYTE.toByte(8)); + vm.setPlatform("Linux"); + vm.setRootVolumeUuid(rootVolumeUuid); + vm.setState(VmInstanceState.Stopped.toString()); + vm.setType(VmInstanceConstant.USER_VM_TYPE); + vm.setLastOpDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + vm.setZoneUuid(uuid()); + + VolumeInventory vol = new VolumeInventory(); + vol.setName(String.format("Root-Volume-For-VM-%s", vm.getUuid())); + vol.setCreateDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + vol.setLastOpDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + vol.setType(VolumeType.Root.toString()); + vol.setUuid(rootVolumeUuid); + vol.setSize(SizeUnit.GIGABYTE.toByte(100)); + vol.setActualSize(SizeUnit.GIGABYTE.toByte(20)); + vol.setDeviceId(0); + vol.setState(VolumeState.Enabled.toString()); + vol.setFormat("qcow2"); + vol.setDiskOfferingUuid(uuid()); + vol.setInstallPath(String.format("/zstack_ps/rootVolumes/acct-36c27e8ff05c4780bf6d2fa65700f22e/vol-%s/%s.qcow2", rootVolumeUuid, rootVolumeUuid)); + vol.setStatus(VolumeStatus.Ready.toString()); + vol.setPrimaryStorageUuid(uuid()); + vol.setVmInstanceUuid(vm.getUuid()); + vol.setRootImageUuid(vm.getImageUuid()); + vm.setAllVolumes(asList(vol)); + + VmNicInventory nic = new VmNicInventory(); + nic.setVmInstanceUuid(vm.getUuid()); + nic.setCreateDate(vm.getCreateDate()); + nic.setLastOpDate(vm.getLastOpDate()); + nic.setDeviceId(0); + nic.setL3NetworkUuid(defaultL3Uuid); + nic.setMac("00:0c:29:bd:99:fc"); + nic.setHypervisorType("KVM"); + nic.setUuid(uuid()); + vm.setVmNics(asList(nic)); + + event.setInventory(vm); + + return event; + } + +} 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..9b208366107 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java @@ -0,0 +1,27 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +public class CleanupVmInstanceMetadataOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private List vmUuids; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } +} 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..9146991af1b --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java @@ -0,0 +1,36 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +import java.util.ArrayList; +import java.util.List; + +public class CleanupVmInstanceMetadataOnPrimaryStorageReply extends MessageReply { + private int totalCleaned; + private int totalFailed; + private List failedVmUuids = new ArrayList<>(); + + public int getTotalCleaned() { + return totalCleaned; + } + + public void setTotalCleaned(int totalCleaned) { + this.totalCleaned = totalCleaned; + } + + public int getTotalFailed() { + return totalFailed; + } + + public void setTotalFailed(int totalFailed) { + this.totalFailed = totalFailed; + } + + public List getFailedVmUuids() { + return failedVmUuids; + } + + public void setFailedVmUuids(List failedVmUuids) { + this.failedVmUuids = failedVmUuids; + } +} 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..d08098380d9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,17 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + + +public class GetVmInstanceMetadataFromPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } +} 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..cfa378a28e9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java @@ -0,0 +1,20 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GetVmInstanceMetadataFromPrimaryStorageReply extends MessageReply { + private List vmInstanceMetadata = new ArrayList<>(); + + public List getVmInstanceMetadata() { + return vmInstanceMetadata; + } + + public void setVmInstanceMetadata(List vmInstanceMetadata) { + this.vmInstanceMetadata = vmInstanceMetadata; + } +} 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/ReadVmInstanceMetadataOnHypervisorMsg.java b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorMsg.java new file mode 100644 index 00000000000..d5d43cb36ea --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorMsg.java @@ -0,0 +1,26 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; + +public class ReadVmInstanceMetadataOnHypervisorMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + private String metadataPath; + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + @Override + public String getHostUuid() { + return hostUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorReply.java b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorReply.java new file mode 100644 index 00000000000..25044b944a6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataOnHypervisorReply.java @@ -0,0 +1,15 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class ReadVmInstanceMetadataOnHypervisorReply 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/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/vm/APICheckVmInstanceMetadataConsistencyMsg.java b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyMsg.java new file mode 100644 index 00000000000..df4da4eee67 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyMsg.java @@ -0,0 +1,56 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.storage.primary.PrimaryStorageVO; + +import java.util.List; + +@RestRequest( + path = "/vm-instances/metadata/consistency-check", + method = HttpMethod.PUT, + responseClass = APICheckVmInstanceMetadataConsistencyReply.class, + isAction = true +) +public class APICheckVmInstanceMetadataConsistencyMsg extends APISyncCallMessage { + @APIParam(required = false, resourceType = VmInstanceVO.class) + private List vmUuids; + + @APIParam(required = false, resourceType = PrimaryStorageVO.class) + private String primaryStorageUuid; + + @APIParam(required = false) + private Boolean autoRepair; + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public Boolean getAutoRepair() { + return autoRepair; + } + + public void setAutoRepair(Boolean autoRepair) { + this.autoRepair = autoRepair; + } + + public static APICheckVmInstanceMetadataConsistencyMsg __example__() { + APICheckVmInstanceMetadataConsistencyMsg msg = new APICheckVmInstanceMetadataConsistencyMsg(); + msg.autoRepair = false; + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyReply.java b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyReply.java new file mode 100644 index 00000000000..61ac6cf55bf --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyReply.java @@ -0,0 +1,31 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.Collections; +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APICheckVmInstanceMetadataConsistencyReply extends APIReply { + private List results; + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public static APICheckVmInstanceMetadataConsistencyReply __example__() { + APICheckVmInstanceMetadataConsistencyReply reply = new APICheckVmInstanceMetadataConsistencyReply(); + ConsistencyCheckResult result = new ConsistencyCheckResult(); + result.setVmUuid(uuid()); + result.setConsistent(true); + result.setDiffs(Collections.emptyList()); + result.setAction("NONE"); + reply.results = Collections.singletonList(result); + return reply; + } +} 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/APICleanupVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..a8003a15428 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java @@ -0,0 +1,44 @@ +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 org.zstack.header.storage.primary.PrimaryStorageVO; + +import java.util.List; + +@RestRequest( + path = "/vm-instances/metadata/cleanup", + method = HttpMethod.PUT, + responseClass = APICleanupVmInstanceMetadataEvent.class, + isAction = true +) +public class APICleanupVmInstanceMetadataMsg extends APIMessage { + @APIParam(required = false, resourceType = PrimaryStorageVO.class) + private List primaryStorageUuids; + + @APIParam(required = false, resourceType = VmInstanceVO.class) + private List vmUuids; + + public List getPrimaryStorageUuids() { + return primaryStorageUuids; + } + + public void setPrimaryStorageUuids(List primaryStorageUuids) { + this.primaryStorageUuids = primaryStorageUuids; + } + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } + + public static APICleanupVmInstanceMetadataMsg __example__() { + APICleanupVmInstanceMetadataMsg msg = new APICleanupVmInstanceMetadataMsg(); + return msg; + } +} 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..d091c517a45 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java @@ -14,6 +14,7 @@ responseClass = APIConvertTemplatedVmInstanceToVmInstanceEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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..62fba128eb1 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java @@ -13,6 +13,7 @@ responseClass = APIConvertVmInstanceToTemplatedVmInstanceEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/APIPreCheckVmMetadataRegistrationMsg.java b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java new file mode 100644 index 00000000000..68912504843 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java @@ -0,0 +1,79 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.cluster.ClusterVO; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.zone.ZoneVO; + +@RestRequest( + path = "/vm-instances/metadata/pre-check", + method = HttpMethod.PUT, + responseClass = APIPreCheckVmMetadataRegistrationReply.class, + isAction = true +) +public class APIPreCheckVmMetadataRegistrationMsg extends APISyncCallMessage { + @APIParam + private String metadataContent; + + @APIParam(resourceType = PrimaryStorageVO.class) + private String targetPrimaryStorageUuid; + + @APIParam(required = false, resourceType = ZoneVO.class) + private String zoneUuid; + + @APIParam(required = false, resourceType = ClusterVO.class) + private String clusterUuid; + + @APIParam(required = false) + private Boolean forceVersionMismatch; + + public String getMetadataContent() { + return metadataContent; + } + + public void setMetadataContent(String metadataContent) { + this.metadataContent = metadataContent; + } + + public String getTargetPrimaryStorageUuid() { + return targetPrimaryStorageUuid; + } + + public void setTargetPrimaryStorageUuid(String targetPrimaryStorageUuid) { + this.targetPrimaryStorageUuid = targetPrimaryStorageUuid; + } + + 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 Boolean getForceVersionMismatch() { + return forceVersionMismatch; + } + + public void setForceVersionMismatch(Boolean forceVersionMismatch) { + this.forceVersionMismatch = forceVersionMismatch; + } + + public static APIPreCheckVmMetadataRegistrationMsg __example__() { + APIPreCheckVmMetadataRegistrationMsg msg = new APIPreCheckVmMetadataRegistrationMsg(); + msg.metadataContent = "{\"schemaVersion\":\"1.0\",\"vmUuid\":\"...\"}"; + msg.targetPrimaryStorageUuid = uuid(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationReply.java b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationReply.java new file mode 100644 index 00000000000..6a0e7efdb3d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationReply.java @@ -0,0 +1,30 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.Collections; +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIPreCheckVmMetadataRegistrationReply extends APIReply { + private List checkResults; + + public List getCheckResults() { + return checkResults; + } + + public void setCheckResults(List checkResults) { + this.checkResults = checkResults; + } + + public static APIPreCheckVmMetadataRegistrationReply __example__() { + APIPreCheckVmMetadataRegistrationReply reply = new APIPreCheckVmMetadataRegistrationReply(); + PreCheckItem item = new PreCheckItem(); + item.setName("schema_version_check"); + item.setPassed(true); + item.setMessage("Schema version 1.0 is supported"); + reply.checkResults = Collections.singletonList(item); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..db0ba2966cd --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java @@ -0,0 +1,48 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.storage.primary.PrimaryStorageVO; + +@RestRequest( + path = "/vm-instances/{vmUuid}/metadata", + method = HttpMethod.GET, + responseClass = APIReadVmInstanceMetadataReply.class +) +public class APIReadVmInstanceMetadataMsg extends APISyncCallMessage implements VmInstanceMessage { + @APIParam(resourceType = VmInstanceVO.class) + private String vmUuid; + + @APIParam(resourceType = PrimaryStorageVO.class) + private String primaryStorageUuid; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + @Override + public String getVmInstanceUuid() { + return vmUuid; + } + + public static APIReadVmInstanceMetadataMsg __example__() { + APIReadVmInstanceMetadataMsg msg = new APIReadVmInstanceMetadataMsg(); + msg.vmUuid = uuid(); + msg.primaryStorageUuid = uuid(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataReply.java b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataReply.java new file mode 100644 index 00000000000..64726c7a8e6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataReply.java @@ -0,0 +1,65 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIReadVmInstanceMetadataReply extends APIReply { + private String metadataContent; + private String schemaVersion; + private String readStatus; + private String repairAction; + private List warnings; + + public String getMetadataContent() { + return metadataContent; + } + + public void setMetadataContent(String metadataContent) { + this.metadataContent = metadataContent; + } + + public String getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public String getReadStatus() { + return readStatus; + } + + public void setReadStatus(String readStatus) { + this.readStatus = readStatus; + } + + public String getRepairAction() { + return repairAction; + } + + public void setRepairAction(String repairAction) { + this.repairAction = repairAction; + } + + public List getWarnings() { + return warnings; + } + + public void setWarnings(List warnings) { + this.warnings = warnings; + } + + public static APIReadVmInstanceMetadataReply __example__() { + APIReadVmInstanceMetadataReply reply = new APIReadVmInstanceMetadataReply(); + reply.metadataContent = "{\"schemaVersion\":\"1.0\",\"vmUuid\":\"...\"}"; + reply.schemaVersion = "1.0"; + reply.readStatus = "OK"; + reply.repairAction = "NONE"; + reply.warnings = java.util.Collections.emptyList(); + return reply; + } +} 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..db685ce85f2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java @@ -0,0 +1,45 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(allTo = "inventory") +public class APIRegisterVmInstanceFromMetadataEvent extends APIEvent { + private VmInstanceInventory inventory; + private List warnings; + + public APIRegisterVmInstanceFromMetadataEvent() { + super(null); + } + + public APIRegisterVmInstanceFromMetadataEvent(String apiId) { + super(apiId); + } + + public VmInstanceInventory getInventory() { + return inventory; + } + + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + + public List getWarnings() { + return warnings; + } + + public void setWarnings(List warnings) { + this.warnings = warnings; + } + + 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/APIRegisterVmInstanceFromMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java new file mode 100644 index 00000000000..11e3a8a0db5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java @@ -0,0 +1,88 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +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.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 = 3) +public class APIRegisterVmInstanceFromMetadataMsg extends APICreateMessage { + @APIParam + private String metadataContent; + + @APIParam(resourceType = PrimaryStorageVO.class) + private String targetPrimaryStorageUuid; + + @APIParam(resourceType = ZoneVO.class) + private String zoneUuid; + + @APIParam(resourceType = ClusterVO.class) + private String clusterUuid; + + @APIParam(required = false) + private Boolean forceVersionMismatch; + + public String getMetadataContent() { + return metadataContent; + } + + public void setMetadataContent(String metadataContent) { + this.metadataContent = metadataContent; + } + + public String getTargetPrimaryStorageUuid() { + return targetPrimaryStorageUuid; + } + + public void setTargetPrimaryStorageUuid(String targetPrimaryStorageUuid) { + this.targetPrimaryStorageUuid = targetPrimaryStorageUuid; + } + + 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 Boolean getForceVersionMismatch() { + return forceVersionMismatch; + } + + public void setForceVersionMismatch(Boolean forceVersionMismatch) { + this.forceVersionMismatch = forceVersionMismatch; + } + + public static APIRegisterVmInstanceFromMetadataMsg __example__() { + APIRegisterVmInstanceFromMetadataMsg msg = new APIRegisterVmInstanceFromMetadataMsg(); + msg.metadataContent = "{\"schemaVersion\":\"1.0\",\"vmUuid\":\"...\"}"; + msg.targetPrimaryStorageUuid = uuid(); + msg.zoneUuid = uuid(); + msg.clusterUuid = uuid(); + msg.forceVersionMismatch = false; + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..e9e3cb26c33 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java @@ -0,0 +1,45 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.storage.primary.PrimaryStorageVO; + +import java.util.List; + +@RestRequest( + path = "/vm-instances/metadata/scan", + method = HttpMethod.GET, + responseClass = APIScanVmInstanceMetadataReply.class +) +public class APIScanVmInstanceMetadataMsg extends APISyncCallMessage { + @APIParam(required = false, resourceType = PrimaryStorageVO.class) + private List primaryStorageUuids; + + @APIParam(required = false, resourceType = VmInstanceVO.class) + private List vmUuids; + + public List getPrimaryStorageUuids() { + return primaryStorageUuids; + } + + public void setPrimaryStorageUuids(List primaryStorageUuids) { + this.primaryStorageUuids = primaryStorageUuids; + } + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } + + public static APIScanVmInstanceMetadataMsg __example__() { + APIScanVmInstanceMetadataMsg msg = new APIScanVmInstanceMetadataMsg(); + msg.primaryStorageUuids = null; + msg.vmUuids = null; + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataReply.java b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataReply.java new file mode 100644 index 00000000000..060d706c6c3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataReply.java @@ -0,0 +1,35 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIScanVmInstanceMetadataReply extends APIReply { + private List metadataList; + + public List getMetadataList() { + return metadataList; + } + + public void setMetadataList(List metadataList) { + this.metadataList = metadataList; + } + + public static APIScanVmInstanceMetadataReply __example__() { + APIScanVmInstanceMetadataReply reply = new APIScanVmInstanceMetadataReply(); + VmMetadataScanResult result = new VmMetadataScanResult(); + result.setVmUuid(uuid()); + result.setVmName("test-vm"); + result.setVmCategory("UserVm"); + result.setPrimaryStorageUuid(uuid()); + result.setPrimaryStorageType("SharedBlock"); + result.setSchemaVersion("1.0"); + result.setLastUpdateTime(System.currentTimeMillis()); + result.setMetadataPath("/dev/zvmdata-xxxxx/vm-metadata-xxxxx"); + result.setSizeBytes(4096L); + reply.metadataList = java.util.Collections.singletonList(result); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataEvent.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataEvent.java new file mode 100644 index 00000000000..9eb4e932014 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataEvent.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 APIUpdateVmMetadataEvent extends APIEvent { + public APIUpdateVmMetadataEvent() { + super(null); + } + + public APIUpdateVmMetadataEvent(String apiId) { + super(apiId); + } + + public static APIUpdateVmMetadataEvent __example__() { + return new APIUpdateVmMetadataEvent(); + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java new file mode 100644 index 00000000000..c04a6a59d23 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java @@ -0,0 +1,36 @@ +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 = "/vm-instances/{vmUuid}/metadata/actions", + method = HttpMethod.PUT, + responseClass = APIUpdateVmMetadataEvent.class, + isAction = true +) +public class APIUpdateVmMetadataMsg extends APIMessage implements VmInstanceMessage { + @APIParam(resourceType = VmInstanceVO.class) + private String vmUuid; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + @Override + public String getVmInstanceUuid() { + return vmUuid; + } + + public static APIUpdateVmMetadataMsg __example__() { + APIUpdateVmMetadataMsg msg = new APIUpdateVmMetadataMsg(); + msg.vmUuid = uuid(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusMsg.java b/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusMsg.java new file mode 100644 index 00000000000..66c88fb97e4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusMsg.java @@ -0,0 +1,39 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +/** + * 批量检查多个 VM 元数据 Header 状态的内部消息(健康巡检)。 + * + *

由管理平面发送给主存储 handler,Agent 端仅读取 Header(不读 Slot), + * 返回每个 VM 的 readStatus 和 PendingOp 信息。

+ * + *

路由:{@code makeLocalServiceId} → 主存储 handler → Agent HTTP 调用

+ * + * @see BatchCheckMetadataStatusReply + * @see MetadataStatusResult + */ +public class BatchCheckMetadataStatusMsg extends NeedReplyMessage { + + private String primaryStorageUuid; + + private List vmUuids; + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusReply.java b/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusReply.java new file mode 100644 index 00000000000..24fea84c40d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/BatchCheckMetadataStatusReply.java @@ -0,0 +1,26 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +import java.util.Map; + +/** + * {@link BatchCheckMetadataStatusMsg} 的回复。 + * + * @see MetadataStatusResult + */ +public class BatchCheckMetadataStatusReply extends MessageReply { + + /** + * key = vmUuid, value = 该 VM 的元数据状态结果。 + */ + private Map results; + + public Map getResults() { + return results; + } + + public void setResults(Map results) { + this.results = results; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/ConsistencyCheckResult.java b/header/src/main/java/org/zstack/header/vm/ConsistencyCheckResult.java new file mode 100644 index 00000000000..fb00307bbe5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/ConsistencyCheckResult.java @@ -0,0 +1,43 @@ +package org.zstack.header.vm; + +import java.io.Serializable; +import java.util.List; + +public class ConsistencyCheckResult implements Serializable { + private String vmUuid; + private boolean consistent; + private List diffs; + private String action; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public boolean isConsistent() { + return consistent; + } + + public void setConsistent(boolean consistent) { + this.consistent = consistent; + } + + public List getDiffs() { + return diffs; + } + + public void setDiffs(List diffs) { + this.diffs = diffs; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/DiffEntry.java b/header/src/main/java/org/zstack/header/vm/DiffEntry.java new file mode 100644 index 00000000000..c1b5e3cbdcd --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/DiffEntry.java @@ -0,0 +1,33 @@ +package org.zstack.header.vm; + +import java.io.Serializable; + +public class DiffEntry implements Serializable { + private String field; + private String expected; + private String actual; + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getExpected() { + return expected; + } + + public void setExpected(String expected) { + this.expected = expected; + } + + public String getActual() { + return actual; + } + + public void setActual(String actual) { + this.actual = actual; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/MetadataStatusResult.java b/header/src/main/java/org/zstack/header/vm/MetadataStatusResult.java new file mode 100644 index 00000000000..c26c684ac5b --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/MetadataStatusResult.java @@ -0,0 +1,65 @@ +package org.zstack.header.vm; + +import java.io.Serializable; + +/** + * 单个 VM 的元数据 Header 状态结果(用于健康巡检)。 + * + * @see BatchCheckMetadataStatusReply + */ +public class MetadataStatusResult implements Serializable { + + /** + * 读取状态:OK / NEED_REPAIR / RECOVERED / DEGRADED / + * STORAGE_CHANGE_INCOMPLETE / CORRUPTED + */ + private String readStatus; + + /** + * 可为 null。NEED_REPAIR/RECOVERED 时提示的修复动作 + * (如 "complete_phase3" / "rebuild_header" / "full_refresh")。 + */ + private String repairAction; + + /** + * 最后更新时间戳(epoch ms)。 + */ + private Long lastUpdateTime; + + /** + * 当前 PendingOp 值(0/1/2)。 + */ + private Integer pendingOp; + + public String getReadStatus() { + return readStatus; + } + + public void setReadStatus(String readStatus) { + this.readStatus = readStatus; + } + + public String getRepairAction() { + return repairAction; + } + + public void setRepairAction(String repairAction) { + this.repairAction = repairAction; + } + + public Long getLastUpdateTime() { + return lastUpdateTime; + } + + public void setLastUpdateTime(Long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + public Integer getPendingOp() { + return pendingOp; + } + + public void setPendingOp(Integer pendingOp) { + this.pendingOp = pendingOp; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/PreCheckItem.java b/header/src/main/java/org/zstack/header/vm/PreCheckItem.java new file mode 100644 index 00000000000..49dc2239b2d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/PreCheckItem.java @@ -0,0 +1,33 @@ +package org.zstack.header.vm; + +import java.io.Serializable; + +public class PreCheckItem implements Serializable { + private String name; + private boolean passed; + private String message; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isPassed() { + return passed; + } + + public void setPassed(boolean passed) { + this.passed = passed; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerMsg.java b/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerMsg.java new file mode 100644 index 00000000000..9b7bfabe21a --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerMsg.java @@ -0,0 +1,64 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * Internal message for LongJob integration of VM registration from metadata. + * Mirrors fields from APIRegisterVmInstanceFromMetadataMsg. + */ +public class RegisterVmFromMetadataInnerMsg extends NeedReplyMessage { + private String metadataContent; + private String targetPrimaryStorageUuid; + private String zoneUuid; + private String clusterUuid; + private Boolean forceVersionMismatch; + private String accountUuid; + + public String getMetadataContent() { + return metadataContent; + } + + public void setMetadataContent(String metadataContent) { + this.metadataContent = metadataContent; + } + + public String getTargetPrimaryStorageUuid() { + return targetPrimaryStorageUuid; + } + + public void setTargetPrimaryStorageUuid(String targetPrimaryStorageUuid) { + this.targetPrimaryStorageUuid = targetPrimaryStorageUuid; + } + + 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 Boolean getForceVersionMismatch() { + return forceVersionMismatch; + } + + public void setForceVersionMismatch(Boolean forceVersionMismatch) { + this.forceVersionMismatch = forceVersionMismatch; + } + + public String getAccountUuid() { + return accountUuid; + } + + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerReply.java b/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerReply.java new file mode 100644 index 00000000000..8353aa880e2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/RegisterVmFromMetadataInnerReply.java @@ -0,0 +1,26 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +import java.util.List; + +public class RegisterVmFromMetadataInnerReply extends MessageReply { + private VmInstanceInventory inventory; + private List warnings; + + public VmInstanceInventory getInventory() { + return inventory; + } + + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + + public List getWarnings() { + return warnings; + } + + public void setWarnings(List warnings) { + this.warnings = warnings; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/RepairMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/RepairMetadataMsg.java new file mode 100644 index 00000000000..212aa76c088 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/RepairMetadataMsg.java @@ -0,0 +1,52 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * 修复 sblk 元数据 Header 的内部消息。 + * + *

由管理平面发送给主存储 handler,用于完成未完成的 Phase 3、 + * 清除 PendingOp、重建 Header 或触发全量刷写。

+ * + *

路由:{@code makeLocalServiceId} → 主存储 handler → Agent HTTP 调用

+ * + * @see BatchCheckMetadataStatusMsg + */ +public class RepairMetadataMsg extends NeedReplyMessage { + + private String vmUuid; + + private String primaryStorageUuid; + + /** + * 修复动作。 + * + *

可选值:{@code complete_phase3} / {@code clear_pending_op} / + * {@code rebuild_header} / {@code full_refresh}

+ */ + private String repairAction; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getRepairAction() { + return repairAction; + } + + public void setRepairAction(String repairAction) { + this.repairAction = repairAction; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/RepairMetadataReply.java b/header/src/main/java/org/zstack/header/vm/RepairMetadataReply.java new file mode 100644 index 00000000000..16445a19fe6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/RepairMetadataReply.java @@ -0,0 +1,11 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +/** + * {@link RepairMetadataMsg} 的回复。 + * + *

成功/失败通过 {@link MessageReply} 基类的 ErrorCode 传递。

+ */ +public class RepairMetadataReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..e918ad6b075 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataMsg.java @@ -0,0 +1,43 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * 更新虚拟机元数据消息(MN 内部)。 + * + *

调用链第 1 步:由 API 完成后的拦截器发出,路由到 VM 所在的 MN 节点。 + * 接收方从 DB 构建 {@link VmInstanceMetadataDTO},编码后发送 + * {@link UpdateVmInstanceMetadataOnPrimaryStorageMsg}。

+ * + * @see UpdateVmInstanceMetadataOnPrimaryStorageMsg + * @see UpdateVmInstanceMetadataOnHypervisorMsg + */ +public class UpdateVmInstanceMetadataMsg extends NeedReplyMessage implements VmInstanceMessage { + + private String vmInstanceUuid; + + /** + * 是否涉及存储结构变更。 + * + *

对应 {@link MetadataImpact.Impact#STORAGE} 类型的操作。 + * sblk 场景下会设置 pending_op=2。

+ */ + 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/UpdateVmInstanceMetadataOnHypervisorMsg.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorMsg.java new file mode 100644 index 00000000000..61e0ebde900 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorMsg.java @@ -0,0 +1,86 @@ +package org.zstack.header.vm; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; + +/** + * 在 Hypervisor 上更新虚拟机元数据消息。 + * + *

调用链第 3 步(可选):发送到 Host Agent 执行实际的存储写入。

+ * + *

使用场景

+ *
    + *
  • sblk:需要通过 Host Agent 操作 LV(activate → write → deactivate)
  • + *
  • local:数据在本地磁盘,需要通过 Host Agent 写入
  • + *
  • NFS:通常通过 PS Agent 直接操作,不使用此消息
  • + *
+ * + * @see UpdateVmInstanceMetadataMsg + * @see UpdateVmInstanceMetadataOnPrimaryStorageMsg + */ +public class UpdateVmInstanceMetadataOnHypervisorMsg extends NeedReplyMessage implements HostMessage { + + private String hostUuid; + private String vmInstanceUuid; + + /** + * 元数据文件在存储上的路径。 + * + *
    + *
  • sblk:LV 设备路径,如 /dev/{vg_uuid}/{vm_uuid}_vmmeta
  • + *
  • local:本地文件路径,如 /path/to/vm/vm_metadata.json
  • + *
+ */ + private String metadataPath; + + /** + * 元数据 JSON 字符串。 + */ + private String metadata; + + /** + * 是否涉及存储结构变更(sblk 场景设置 pending_op=2)。 + */ + private boolean storageStructureChange; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + public boolean isStorageStructureChange() { + return storageStructureChange; + } + + public void setStorageStructureChange(boolean storageStructureChange) { + this.storageStructureChange = storageStructureChange; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorReply.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorReply.java new file mode 100644 index 00000000000..036403b01b2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnHypervisorReply.java @@ -0,0 +1,9 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +/** + * {@link UpdateVmInstanceMetadataOnHypervisorMsg} 的回复。 + */ +public class UpdateVmInstanceMetadataOnHypervisorReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java new file mode 100644 index 00000000000..41ba6d9b254 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java @@ -0,0 +1,84 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.storage.primary.PrimaryStorageMessage; + +/** + * 在主存储上更新虚拟机元数据消息。 + * + *

调用链第 2 步:发送到主存储服务,由主存储根据自身类型决定写入方式: + *

    + *
  • sblk/local:进一步发送 {@link UpdateVmInstanceMetadataOnHypervisorMsg} 到 Host Agent
  • + *
  • NFS:直接通过 PS Agent 写入
  • + *
+ * + * @see UpdateVmInstanceMetadataMsg + * @see UpdateVmInstanceMetadataOnHypervisorMsg + */ +public class UpdateVmInstanceMetadataOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + + private String primaryStorageUuid; + private String vmInstanceUuid; + + /** + * 根盘 UUID,用于 PS handler 定位元数据写入路径。 + * + *

LocalStorage 通过根盘 installPath 推导元数据文件路径; + * NFS 通过根盘关联的 Host 确定转发目标。

+ */ + private String rootVolumeUuid; + + /** + * 元数据 JSON 字符串。 + * + *

由 {@code VmInstanceBase.buildVmInstanceMetadata()} 从 DB 全量构建, + * 为 {@link VmInstanceMetadataDTO} 的 JSON 序列化结果。

+ */ + private String metadata; + + /** + * 是否涉及存储结构变更(sblk 场景设置 pending_op=2)。 + */ + private boolean storageStructureChange; + + @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 getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + public boolean isStorageStructureChange() { + return storageStructureChange; + } + + public void setStorageStructureChange(boolean storageStructureChange) { + this.storageStructureChange = storageStructureChange; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageReply.java new file mode 100644 index 00000000000..475855f2b67 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataOnPrimaryStorageReply.java @@ -0,0 +1,9 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +/** + * {@link UpdateVmInstanceMetadataOnPrimaryStorageMsg} 的回复。 + */ +public class UpdateVmInstanceMetadataOnPrimaryStorageReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataReply.java b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataReply.java new file mode 100644 index 00000000000..61a2d4dbd1b --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/UpdateVmInstanceMetadataReply.java @@ -0,0 +1,9 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.MessageReply; + +/** + * {@link UpdateVmInstanceMetadataMsg} 的回复。 + */ +public class UpdateVmInstanceMetadataReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataRegistrationSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataRegistrationSpec.java new file mode 100644 index 00000000000..a3ea3981759 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceMetadataRegistrationSpec.java @@ -0,0 +1,92 @@ +package org.zstack.header.vm; + +/** + * 虚拟机元数据注册参数。 + * + *

封装从元数据注册虚拟机时需要的新环境上下文信息。

+ * + *

字段处理矩阵中标记为"API 参数"或"替换"的字段,其新值来源于此对象。

+ */ +public class VmInstanceMetadataRegistrationSpec { + + /** + * 注册目标 Zone UUID(必填)。 + * + *

替换 VmInstanceVO.zoneUuid。

+ */ + private String zoneUuid; + + /** + * 注册目标主存储 UUID(必填)。 + * + *

替换 VolumeVO.primaryStorageUuid、VolumeSnapshotVO.primaryStorageUuid。

+ */ + private String primaryStorageUuid; + + /** + * 注册操作的账户 UUID。 + * + *

替换所有 VO 的 accountUuid 字段。通常为 admin。

+ */ + private String accountUuid; + + /** + * 旧存储路径标识符。 + * + *
    + *
  • sblk 场景:旧 VG UUID
  • + *
  • local/NFS 场景:旧路径前缀(如 /vms_ds)
  • + *
+ */ + private String oldPathIdentifier; + + /** + * 新存储路径标识符。 + * + *
    + *
  • sblk 场景:新 VG UUID
  • + *
  • local/NFS 场景:新路径前缀(如 /vms_ds2)
  • + *
+ */ + private String newPathIdentifier; + + public String getZoneUuid() { + return zoneUuid; + } + + public void setZoneUuid(String zoneUuid) { + this.zoneUuid = zoneUuid; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getAccountUuid() { + return accountUuid; + } + + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } + + public String getOldPathIdentifier() { + return oldPathIdentifier; + } + + public void setOldPathIdentifier(String oldPathIdentifier) { + this.oldPathIdentifier = oldPathIdentifier; + } + + public String getNewPathIdentifier() { + return newPathIdentifier; + } + + public void setNewPathIdentifier(String newPathIdentifier) { + this.newPathIdentifier = newPathIdentifier; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmMetadataScanResult.java b/header/src/main/java/org/zstack/header/vm/VmMetadataScanResult.java new file mode 100644 index 00000000000..8a007b3df34 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmMetadataScanResult.java @@ -0,0 +1,87 @@ +package org.zstack.header.vm; + +import java.io.Serializable; + +public class VmMetadataScanResult implements Serializable { + private String vmUuid; + private String vmName; + private String vmCategory; + private String primaryStorageUuid; + private String primaryStorageType; + private String schemaVersion; + private Long lastUpdateTime; + private String metadataPath; + private Long sizeBytes; + + 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 getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getPrimaryStorageType() { + return primaryStorageType; + } + + public void setPrimaryStorageType(String primaryStorageType) { + this.primaryStorageType = primaryStorageType; + } + + public String getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public Long getLastUpdateTime() { + return lastUpdateTime; + } + + public void setLastUpdateTime(Long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public Long getSizeBytes() { + return sizeBytes; + } + + public void setSizeBytes(Long sizeBytes) { + this.sizeBytes = sizeBytes; + } +} diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceAction.java new file mode 100644 index 00000000000..1fb295bf5ae --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RegisterVmInstanceAction 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.RegisterVmInstanceResult 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 = 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 primaryStorageUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String metadataPath; + + @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.RegisterVmInstanceResult value = res.getResult(org.zstack.sdk.RegisterVmInstanceResult.class); + ret.value = value == null ? new org.zstack.sdk.RegisterVmInstanceResult() : 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/register"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceResult.java b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceResult.java new file mode 100644 index 00000000000..49510a84cb9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmInstanceInventory; + +public class RegisterVmInstanceResult { + public VmInstanceInventory inventory; + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + public VmInstanceInventory getInventory() { + return this.inventory; + } + +} From 5ca9f42af22a145a1ced28f70158e0c8ecfc4c76 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:48:03 +0800 Subject: [PATCH 03/10] [vm-metadata]: @MetadataImpact annotation and API interception - MetadataImpact annotation with Impact enum (STORAGE/CONFIG/NIC/ALL) - VmUuidFromApiResolver interface for VM UUID extraction - 8 resolver implementations: Default, NicBased, ReflectionBased, ResourceBased, SnapshotBased, SnapshotGroupBased, VolumeBased - VmMetadataUpdateInterceptor: intercepts annotated APIs to markDirty - @MetadataImpact annotations on 30 existing API messages: VM, Volume, Snapshot, SnapshotGroup, Tag, ResourceConfig, CdRom, Backup, LocalStorage APIs - VmInstanceApiInterceptor metadata validation Resolves: ZSV-10000 Part: 01b --- .../compute/vm/VmInstanceApiInterceptor.java | 1 + .../DefaultVmUuidFromApiResolver.java | 27 ++ .../NicBasedVmUuidFromApiResolver.java | 127 +++++++ .../ReflectionBasedVmUuidFromApiResolver.java | 59 +++ .../ResourceBasedVmUuidFromApiResolver.java | 158 ++++++++ .../SnapshotBasedVmUuidFromApiResolver.java | 54 +++ ...apshotGroupBasedVmUuidFromApiResolver.java | 62 +++ .../metadata/VmMetadataUpdateInterceptor.java | 358 ++++++++++++++++++ .../VolumeBasedVmUuidFromApiResolver.java | 66 ++++ .../APIExportImageFromBackupStorageMsg.java | 2 + .../snapshot/APIDeleteVolumeSnapshotMsg.java | 2 + .../APIRevertVolumeFromSnapshotMsg.java | 2 + .../APIDeleteVolumeSnapshotGroupMsg.java | 2 + .../header/tag/APICreateSystemTagMsg.java | 2 + .../zstack/header/tag/APIDeleteTagMsg.java | 2 + .../header/tag/APIUpdateSystemTagMsg.java | 2 + .../header/vm/APIAttachVmNicToVmMsg.java | 1 + .../header/vm/APIChangeVmNicNetworkMsg.java | 1 + .../header/vm/APIChangeVmNicStateMsg.java | 1 + .../header/vm/APIDeleteVmBootModeMsg.java | 1 + .../header/vm/APIDeleteVmHostnameMsg.java | 1 + .../zstack/header/vm/APIDeleteVmNicMsg.java | 1 + .../header/vm/APIDeleteVmSshKeyMsg.java | 1 + .../header/vm/APIRecoverVmInstanceMsg.java | 1 + .../header/vm/APIReimageVmInstanceMsg.java | 1 + .../header/vm/APISetVmBootOrderMsg.java | 1 + .../header/vm/APIUpdateVmInstanceMsg.java | 1 + .../org/zstack/header/vm/MetadataImpact.java | 71 ++++ .../header/vm/VmUuidFromApiResolver.java | 49 +++ .../header/vm/cdrom/APIDeleteVmCdRomMsg.java | 2 + .../volume/APIAttachDataVolumeToVmMsg.java | 2 + .../APICreateVolumeSnapshotGroupMsg.java | 2 + .../header/volume/APIDeleteDataVolumeMsg.java | 2 + .../volume/APIDetachDataVolumeFromVmMsg.java | 2 + .../header/volume/APIFlattenVolumeMsg.java | 2 + .../volume/APIRecoverDataVolumeMsg.java | 2 + .../APILocalStorageMigrateVolumeMsg.java | 2 + .../APIDeleteResourceConfigMsg.java | 2 + .../APIUpdateResourceConfigMsg.java | 2 + 39 files changed, 1077 insertions(+) create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/DefaultVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/NicBasedVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/ReflectionBasedVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/ResourceBasedVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotBasedVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotGroupBasedVmUuidFromApiResolver.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataUpdateInterceptor.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/VolumeBasedVmUuidFromApiResolver.java create mode 100644 header/src/main/java/org/zstack/header/vm/MetadataImpact.java create mode 100644 header/src/main/java/org/zstack/header/vm/VmUuidFromApiResolver.java 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..0ce04419dcb 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -21,6 +21,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.network.l2.*; import org.zstack.header.network.l3.*; +import org.zstack.header.storage.primary.APIRegisterVmInstanceMsg; import org.zstack.header.storage.primary.PrimaryStorageClusterRefVO; import org.zstack.header.storage.primary.PrimaryStorageClusterRefVO_; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/DefaultVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/DefaultVmUuidFromApiResolver.java new file mode 100644 index 00000000000..75e3122184a --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/DefaultVmUuidFromApiResolver.java @@ -0,0 +1,27 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; + +import java.util.Collections; +import java.util.List; + +/** + * 默认 VM UUID 解析器:从实现 {@link VmInstanceMessage} 接口的 API 消息中直接获取 vmInstanceUuid。 + * + *

覆盖绝大多数 VM 直接 API(如 APIUpdateVmInstanceMsg、APIStartVmInstanceMsg 等)。

+ */ +public class DefaultVmUuidFromApiResolver implements VmUuidFromApiResolver { + + @Override + public boolean supports(APIMessage msg) { + return msg instanceof VmInstanceMessage; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String vmUuid = ((VmInstanceMessage) msg).getVmInstanceUuid(); + return vmUuid != null ? Collections.singletonList(vmUuid) : Collections.emptyList(); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/NicBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/NicBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..ff0a8a1017a --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/NicBasedVmUuidFromApiResolver.java @@ -0,0 +1,127 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.core.db.SQL; +import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** + * NIC 关联 VM UUID 解析器:从携带 vmNicUuid 的 API 消息中查询 VmNicVO 得到 vmInstanceUuid。 + * + *

覆盖的 API:

+ *
    + *
  • {@code APIChangeVmNicNetworkMsg} — vmNicUuid 字段,实现了 VmInstanceMessage 但 vmInstanceUuid 为 @APINoSee
  • + *
  • {@code APIChangeVmNicStateMsg} — vmNicUuid 字段,同上
  • + *
  • {@code APIDeleteVmNicMsg} — uuid 字段(即 nicUuid),继承 APIDeleteMessage,不实现 VmInstanceMessage
  • + *
+ * + *

解析链

+ *
+ *   vmNicUuid → VmNicVO.vmInstanceUuid
+ * 
+ * + *

设计说明

+ *

由于不存在统一的 VmNicMessage 接口,本解析器通过反射检测消息上的 {@code getVmNicUuid()} 方法获取 nicUuid。 + * 对于 {@code APIDeleteVmNicMsg},其 uuid 字段即为 nicUuid,通过 {@code getUuid()} 获取。

+ * + *

本解析器不处理 BondingMessage 类消息({@code APIAttachNicToBondingMsg}、{@code APIDetachNicFromBondingMsg}), + * 因为 bonding → VM 的解析链涉及 PCI 设备关联,复杂度过高且为边缘场景。 + * 这些消息由 {@link ReflectionBasedVmUuidFromApiResolver} 兜底处理。

+ * + *

解析时机

+ *

在 API 执行前(BeforeDeliveryMessageInterceptor)调用。此时 VmNicVO 仍在数据库中, + * 即使是 delete 场景也能查到关联的 vmInstanceUuid。

+ */ +public class NicBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + private static final CLogger logger = Utils.getLogger(NicBasedVmUuidFromApiResolver.class); + + @Override + public boolean supports(APIMessage msg) { + // 检测消息是否携带 vmNicUuid(通过反射,因无统一接口) + return getVmNicUuid(msg) != null; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String nicUuid = getVmNicUuid(msg); + if (nicUuid == null) { + return Collections.emptyList(); + } + + return SQL.New( + "SELECT n.vmInstanceUuid FROM VmNicVO n " + + "WHERE n.uuid = :nicUuid AND n.vmInstanceUuid IS NOT NULL", + String.class + ).param("nicUuid", nicUuid).list(); + } + + /** + * 尝试从消息中提取 vmNicUuid。 + * + *

按优先级尝试:

+ *
    + *
  1. getVmNicUuid() — APIChangeVmNicNetworkMsg, APIChangeVmNicStateMsg 等
  2. + *
  3. getUuid() 且 resourceType 为 VmNicVO — APIDeleteVmNicMsg
  4. + *
+ */ + private String getVmNicUuid(APIMessage msg) { + // 1. 尝试 getVmNicUuid() + try { + Method method = msg.getClass().getMethod("getVmNicUuid"); + Object result = method.invoke(msg); + if (result instanceof String) { + return (String) result; + } + } catch (NoSuchMethodException ignored) { + // 无此方法,继续尝试 + } catch (Exception e) { + logger.trace(String.format("failed to invoke getVmNicUuid() on %s: %s", + msg.getClass().getSimpleName(), e.getMessage())); + } + + // 2. 尝试 getUuid():针对 APIDeleteVmNicMsg(@APIParam(resourceType = VmNicVO.class)) + // 通过检查 APIParam 注解的 resourceType 确认 uuid 确实是 VmNicVO 的主键 + try { + Method method = msg.getClass().getMethod("getUuid"); + // 检查 uuid 字段上的 @APIParam(resourceType = VmNicVO.class) 注解 + java.lang.reflect.Field field = findField(msg.getClass(), "uuid"); + if (field != null) { + org.zstack.header.message.APIParam apiParam = field.getAnnotation( + org.zstack.header.message.APIParam.class); + if (apiParam != null && "VmNicVO".equals(apiParam.resourceType().getSimpleName())) { + Object result = method.invoke(msg); + if (result instanceof String) { + return (String) result; + } + } + } + } catch (NoSuchMethodException ignored) { + // 无此方法 + } catch (Exception e) { + logger.trace(String.format("failed to check uuid field on %s: %s", + msg.getClass().getSimpleName(), e.getMessage())); + } + + return null; + } + + /** + * 在类层次结构中查找声明的字段(包括父类)。 + */ + private java.lang.reflect.Field findField(Class clazz, String fieldName) { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/ReflectionBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/ReflectionBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..edf07e49a1c --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/ReflectionBasedVmUuidFromApiResolver.java @@ -0,0 +1,59 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** + * 反射兜底解析器:通过反射调用 API 消息的 getVmInstanceUuid() / getResourceUuid() 方法。 + * + *

此解析器作为所有其他 Resolver 的兜底,处理未被显式 Resolver 覆盖但仍然携带 + * vmInstanceUuid 或 resourceUuid 的 API 消息。

+ * + *

注册顺序必须排在所有显式 Resolver 之后(在 XML 中排最后)。

+ */ +public class ReflectionBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + private static final CLogger logger = Utils.getLogger(ReflectionBasedVmUuidFromApiResolver.class); + + @Override + public boolean supports(APIMessage msg) { + // 兜底:对所有消息返回 true,但 resolveVmUuids 可能返回空 + return true; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + // 优先尝试 getVmInstanceUuid() + String vmUuid = invokeGetter(msg, "getVmInstanceUuid"); + if (vmUuid != null) { + return Collections.singletonList(vmUuid); + } + + // fallback: getResourceUuid() + String resourceUuid = invokeGetter(msg, "getResourceUuid"); + if (resourceUuid != null) { + return Collections.singletonList(resourceUuid); + } + + logger.debug(String.format("cannot extract vmInstanceUuid from %s via reflection", msg.getClass().getName())); + return Collections.emptyList(); + } + + private String invokeGetter(Object obj, String methodName) { + try { + Method method = obj.getClass().getMethod(methodName); + return (String) method.invoke(obj); + } catch (NoSuchMethodException e) { + return null; + } catch (Exception e) { + logger.warn(String.format("failed to invoke %s on %s: %s", + methodName, obj.getClass().getName(), e.getMessage())); + return null; + } + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/ResourceBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/ResourceBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..b6da68a0172 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/ResourceBasedVmUuidFromApiResolver.java @@ -0,0 +1,158 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.SQL; +import org.zstack.header.message.APIMessage; +import org.zstack.header.tag.APIAbstractCreateTagMsg; +import org.zstack.header.tag.APIDeleteTagMsg; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmUuidFromApiResolver; +import org.zstack.resourceconfig.APIDeleteResourceConfigMsg; +import org.zstack.resourceconfig.APIUpdateResourceConfigMsg; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Collections; +import java.util.List; + +/** + * 资源关联 VM UUID 解析器:从 SystemTag / ResourceConfig 类 API 消息中解析出关联的 vmInstanceUuid。 + * + *

通过 resourceType + resourceUuid 判断资源所属 VM:

+ *
    + *
  • resourceType=VmInstanceVO → 直接返回 resourceUuid
  • + *
  • resourceType=VolumeVO → 查询 VolumeVO.vmInstanceUuid
  • + *
  • resourceType=VmNicVO → 查询 VmNicVO.vmInstanceUuid
  • + *
  • resourceType=VolumeSnapshotVO → VolumeSnapshotVO.volumeUuid → VolumeVO.vmInstanceUuid
  • + *
  • 其他类型 → 不影响 VM 元数据,返回空
  • + *
+ * + *

注意

+ *

APIDeleteTagMsg 需要先查询 Tag 获取 resourceType/resourceUuid, + * 因此必须在 API 执行前(beforeDeliveryMessage)调用。

+ */ +public class ResourceBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + private static final CLogger logger = Utils.getLogger(ResourceBasedVmUuidFromApiResolver.class); + + @Autowired + private DatabaseFacade dbf; + + @Override + public boolean supports(APIMessage msg) { + return msg instanceof APIAbstractCreateTagMsg + || msg instanceof APIDeleteTagMsg + || msg instanceof APIUpdateResourceConfigMsg + || msg instanceof APIDeleteResourceConfigMsg; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String resourceType = null; + String resourceUuid = null; + + if (msg instanceof APIAbstractCreateTagMsg) { + resourceType = ((APIAbstractCreateTagMsg) msg).getResourceType(); + resourceUuid = ((APIAbstractCreateTagMsg) msg).getResourceUuid(); + } else if (msg instanceof APIDeleteTagMsg) { + // 查询 Tag 获取 resourceType 和 resourceUuid + SystemTagVO tag = dbf.findByUuid(((APIDeleteTagMsg) msg).getUuid(), SystemTagVO.class); + if (tag != null) { + resourceType = tag.getResourceType(); + resourceUuid = tag.getResourceUuid(); + } + } else if (msg instanceof APIUpdateResourceConfigMsg) { + // ResourceConfig API: resourceType 为 ResourceVO(通用基类),需通过 resourceUuid 逐一探测 + return resolveByResourceUuid(((APIUpdateResourceConfigMsg) msg).getResourceUuid()); + } else if (msg instanceof APIDeleteResourceConfigMsg) { + return resolveByResourceUuid(((APIDeleteResourceConfigMsg) msg).getResourceUuid()); + } + + if (resourceType == null || resourceUuid == null) { + return Collections.emptyList(); + } + + return resolveByResourceType(resourceType, resourceUuid); + } + + private List resolveByResourceType(String resourceType, String resourceUuid) { + // 直接关联 VM + if ("VmInstanceVO".equals(resourceType)) { + return Collections.singletonList(resourceUuid); + } + + // Volume → VM + if ("VolumeVO".equals(resourceType)) { + return SQL.New( + "SELECT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid = :uuid AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", resourceUuid).list(); + } + + // VmNic → VM + if ("VmNicVO".equals(resourceType)) { + return SQL.New( + "SELECT n.vmInstanceUuid FROM VmNicVO n " + + "WHERE n.uuid = :uuid AND n.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", resourceUuid).list(); + } + + // VolumeSnapshot → Volume → VM + if ("VolumeSnapshotVO".equals(resourceType)) { + return SQL.New( + "SELECT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid = (SELECT s.volumeUuid FROM VolumeSnapshotVO s WHERE s.uuid = :uuid) " + + "AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", resourceUuid).list(); + } + + // 其他资源类型不影响 VM 元数据 + logger.trace(String.format("resourceType[%s] does not map to VM metadata, skipping", resourceType)); + return Collections.emptyList(); + } + + /** + * ResourceConfig API 的 resourceUuid 类型为 ResourceVO(通用基类),无法从消息中获取具体资源类型。 + * 按优先级逐一探测:VmInstanceVO → VolumeVO → VmNicVO → 空(非 VM 关联资源)。 + * 每步查询命中主键索引,开销 < 1ms。 + */ + private List resolveByResourceUuid(String resourceUuid) { + if (resourceUuid == null) { + return Collections.emptyList(); + } + + // 1. 直接关联 VM + VmInstanceVO vm = dbf.findByUuid(resourceUuid, VmInstanceVO.class); + if (vm != null) { + return Collections.singletonList(resourceUuid); + } + + // 2. Volume → VM + List vmUuids = SQL.New( + "SELECT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid = :uuid AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", resourceUuid).list(); + if (!vmUuids.isEmpty()) { + return vmUuids; + } + + // 3. VmNic → VM + vmUuids = SQL.New( + "SELECT n.vmInstanceUuid FROM VmNicVO n " + + "WHERE n.uuid = :uuid AND n.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", resourceUuid).list(); + if (!vmUuids.isEmpty()) { + return vmUuids; + } + + // 非 VM 关联资源(如 Host、Cluster 等),不触发元数据操作 + logger.trace(String.format("resourceUuid[%s] does not map to any VM, skipping", resourceUuid)); + return Collections.emptyList(); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..fba6b337862 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotBasedVmUuidFromApiResolver.java @@ -0,0 +1,54 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.core.db.SQL; +import org.zstack.header.message.APIMessage; +import org.zstack.header.storage.snapshot.VolumeSnapshotMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; + +import java.util.Collections; +import java.util.List; + +/** + * 快照关联 VM UUID 解析器:从实现 {@link VolumeSnapshotMessage} 接口的 API 消息中获取 snapshotUuid, + * 查询 VolumeSnapshotVO → VolumeVO 得到关联的 vmInstanceUuid。 + * + *

覆盖的 API:

+ *
    + *
  • {@code APIDeleteVolumeSnapshotMsg}(implements DeleteVolumeSnapshotMessage → VolumeSnapshotMessage)
  • + *
  • {@code APIRevertVolumeFromSnapshotMsg}(implements RevertVolumeSnapshotMessage → VolumeSnapshotMessage)
  • + *
  • {@code APIFlattenVolumeMsg} 等直接携带 volumeUuid 的不经过本解析器
  • + *
+ * + *

解析链

+ *
+ *   snapshotUuid → VolumeSnapshotVO.volumeUuid → VolumeVO.vmInstanceUuid
+ * 
+ * + *

解析时机

+ *

在 API 执行前(BeforeDeliveryMessageInterceptor)调用。此时快照和关联的 Volume 仍存在于数据库中, + * 因此无需 pre-capture 机制。对于 delete 场景,快照本身被删除但 Volume 仍在;对于 revert 场景, + * Volume 关联关系不变。

+ */ +public class SnapshotBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + + @Override + public boolean supports(APIMessage msg) { + return msg instanceof VolumeSnapshotMessage; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String snapshotUuid = ((VolumeSnapshotMessage) msg).getSnapshotUuid(); + if (snapshotUuid == null) { + return Collections.emptyList(); + } + + // snapshotUuid → VolumeSnapshotVO.volumeUuid → VolumeVO.vmInstanceUuid + return SQL.New( + "SELECT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid = (SELECT s.volumeUuid FROM VolumeSnapshotVO s WHERE s.uuid = :snapshotUuid) " + + "AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("snapshotUuid", snapshotUuid).list(); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotGroupBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotGroupBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..3245afcfebe --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/SnapshotGroupBasedVmUuidFromApiResolver.java @@ -0,0 +1,62 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.core.db.SQL; +import org.zstack.header.message.APIMessage; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; + +import java.util.Collections; +import java.util.List; + +/** + * 快照组关联 VM UUID 解析器:从实现 {@link VolumeSnapshotGroupMessage} 接口的 API 消息中获取 groupUuid, + * 通过 VolumeSnapshotGroupRefVO 查询关联的所有 volumeUuid,再查询 VolumeVO 得到 vmInstanceUuid。 + * + *

覆盖的 API:

+ *
    + *
  • {@code APIDeleteVolumeSnapshotGroupMsg}(STORAGE, updateOnFailure=true)
  • + *
  • {@code APICreateVolumeSnapshotGroupMsg}(STORAGE, updateOnFailure=true)
  • + *
+ * + *

解析链

+ *
+ *   groupUuid → VolumeSnapshotGroupRefVO.volumeUuid(多个) → VolumeVO.vmInstanceUuid(去重)
+ * 
+ * + *

注意

+ *

一个快照组可能关联多个 Volume(根盘 + 数据盘),这些 Volume 可能分属不同 VM(虽然通常同属一个 VM)。 + * 本解析器返回所有关联 VM 的 uuid 列表。

+ */ +public class SnapshotGroupBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + + @Override + public boolean supports(APIMessage msg) { + return msg instanceof VolumeSnapshotGroupMessage; + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String groupUuid = ((VolumeSnapshotGroupMessage) msg).getGroupUuid(); + if (groupUuid == null) { + return Collections.emptyList(); + } + + // groupUuid → VolumeSnapshotGroupRefVO → volumeUuid 列表 + List volumeUuids = SQL.New( + "SELECT DISTINCT ref.volumeUuid FROM VolumeSnapshotGroupRefVO ref " + + "WHERE ref.volumeSnapshotGroupUuid = :groupUuid", + String.class + ).param("groupUuid", groupUuid).list(); + + if (volumeUuids.isEmpty()) { + return Collections.emptyList(); + } + + // volumeUuid 列表 → VolumeVO.vmInstanceUuid(去重,排除 NULL) + return SQL.New( + "SELECT DISTINCT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid IN (:volumeUuids) AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("volumeUuids", volumeUuids).list(); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataUpdateInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataUpdateInterceptor.java new file mode 100644 index 00000000000..efb4d67d044 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataUpdateInterceptor.java @@ -0,0 +1,358 @@ +package org.zstack.compute.vm.metadata; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.header.Component; +import org.zstack.header.message.*; +import org.zstack.header.vm.MetadataImpact; +import org.zstack.header.vm.VmUuidFromApiResolver; +import org.zstack.utils.BeanUtils; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import org.zstack.core.thread.PeriodicTask; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * 拦截标注了 {@link MetadataImpact} 的 API 消息,在 API 成功后调用 markDirty 触发元数据更新。 + * + *

工作流程

+ *
+ * API Message 投递                          Event 发布
+ *       │                                      │
+ *       ▼                                      ▼
+ * BeforeDeliveryMessageInterceptor    BeforePublishEventInterceptor
+ *       │                                      │
+ *       │  检测 @MetadataImpact               │  通过 apiId 匹配
+ *       │  通过 VmUuidFromApiResolver          │  检查 API 是否成功
+ *       │  解析 vmUuid 列表                    │  调用 markDirty()
+ *       │  缓存到 pendingApis                  │  清理 pendingApis
+ *       │  (key = apiId)                       │
+ *       ▼                                      ▼
+ * 
+ * + *

VM UUID 解析链

+ *

通过注入的 {@link VmUuidFromApiResolver} 列表按顺序解析 vmUuid:

+ *
    + *
  1. {@link DefaultVmUuidFromApiResolver} — VmInstanceMessage 接口
  2. + *
  3. {@link VolumeBasedVmUuidFromApiResolver} — VolumeMessage → 查库
  4. + *
  5. {@link ResourceBasedVmUuidFromApiResolver} — Tag/ResourceConfig API → resourceType/resourceUuid 查库
  6. + *
  7. {@link SnapshotBasedVmUuidFromApiResolver} — VolumeSnapshotMessage → snapshotUuid → volumeUuid → vmUuid
  8. + *
  9. {@link SnapshotGroupBasedVmUuidFromApiResolver} — VolumeSnapshotGroupMessage → groupUuid → vmUuids
  10. + *
  11. {@link NicBasedVmUuidFromApiResolver} — vmNicUuid(反射) → VmNicVO → vmUuid
  12. + *
  13. {@link ReflectionBasedVmUuidFromApiResolver} — 反射兜底
  14. + *
+ * + *

标脏策略

+ *

使用 {@link VmMetadataDirtyMarker#markDirty(String, boolean)} 将 VM 标记为脏, + * INSERT ON DUPLICATE KEY UPDATE 天然去重,100 个 API 只产生 1 行 dirty 行。 + * markDirty 后立即尝试认领并刷写,Poller 作为安全网处理退避和异常场景。

+ */ +public class VmMetadataUpdateInterceptor implements Component, ManagementNodeReadyExtensionPoint { + private static final CLogger logger = Utils.getLogger(VmMetadataUpdateInterceptor.class); + + @Autowired + private CloudBus bus; + + @Autowired + private VmMetadataDirtyMarker dirtyMarker; + + @Autowired + private ThreadFacade thdf; + + /** + * VM UUID 解析器链,按注册顺序尝试(在 VmInstanceManager.xml 中注册)。 + */ + @Autowired(required = false) + private List resolvers = Collections.emptyList(); + + // apiId -> MetadataImpactInfo 映射,在 API 投递时写入,在 Event 发布时消费 + private final Map pendingApis = new ConcurrentHashMap<>(); + + // 标注了 @MetadataImpact 的 API 消息类集合 + private final Set> impactApiClasses = ConcurrentHashMap.newKeySet(); + + /** + * 内部消息注册表:声明"已知会影响 VM 元数据但不经过 API 拦截器"的消息类型。 + * + *

用途:

+ *
    + *
  • 代码审计与 CI 检查:确保所有影响元数据的内部消息已被识别
  • + *
  • 对应 handler 在事务提交后直接调用 {@code markDirty()},不走本拦截器
  • + *
+ * + *

注册方式:各模块在 Component.start() 中调用 + * {@link #registerInternalMetadataMessage(Class)} 注册。

+ * + * @see VmMetadataDirtyMarker#markDirty(String, boolean) + */ + private static final Set> INTERNAL_METADATA_MESSAGES = + Collections.synchronizedSet(new HashSet<>()); + + /** + * 注册一个内部消息类型到 INTERNAL_METADATA_MESSAGES 注册表。 + * 各模块在 Component.start() 中调用此方法。 + * + * @param msgClass 内部消息类型 + */ + public static void registerInternalMetadataMessage(Class msgClass) { + INTERNAL_METADATA_MESSAGES.add(msgClass); + logger.debug(String.format("registered internal metadata message: %s", msgClass.getName())); + } + + /** + * 获取已注册的内部元数据消息类型(只读视图,用于 CI 检查)。 + */ + public static Set> getInternalMetadataMessages() { + return Collections.unmodifiableSet(INTERNAL_METADATA_MESSAGES); + } + + // pendingApis 超时清理任务 + private Future cleanupFuture; + + @Override + public boolean start() { + // 1. 扫描所有标注了 @MetadataImpact 的 API 消息类 + scanMetadataImpactApis(); + + // 2. 注册 BeforeDeliveryMessageInterceptor,在 API 消息被投递处理前, + // 通过 Resolver 链解析 vmInstanceUuid 并缓存 + bus.installBeforeDeliveryMessageInterceptor(new AbstractBeforeDeliveryMessageInterceptor() { + @Override + public void beforeDeliveryMessage(Message msg) { + if (!(msg instanceof APIMessage)) { + return; + } + if (!impactApiClasses.contains(msg.getClass())) { + return; + } + + // vm.metadata 功能总开关,关闭时跳过所有元数据更新 + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + APIMessage apiMsg = (APIMessage) msg; + List vmUuids = resolveVmUuids(apiMsg); + if (vmUuids.isEmpty()) { + return; + } + + MetadataImpact impact = msg.getClass().getAnnotation(MetadataImpact.class); + pendingApis.put(apiMsg.getId(), new MetadataImpactInfo( + vmUuids, impact.value(), impact.updateOnFailure())); + } + }, impactApiClasses.toArray(new Class[0])); + + // 3. 注册 BeforePublishEventInterceptor,在 Event 发布前检查并标脏 + bus.installBeforePublishEventInterceptor(new AbstractBeforePublishEventInterceptor() { + @Override + public void beforePublishEvent(Event evt) { + if (!(evt instanceof APIEvent)) { + return; + } + + APIEvent apiEvent = (APIEvent) evt; + MetadataImpactInfo info = pendingApis.remove(apiEvent.getApiId()); + if (info == null) { + return; + } + + // API 失败则跳过(除非 @MetadataImpact(updateOnFailure=true)) + if (apiEvent.getError() != null && !info.updateOnFailure) { + return; + } + + for (String vmUuid : info.vmUuids) { + submitMarkDirty(vmUuid, info.impact); + } + } + }); + + return true; + } + + private void scanMetadataImpactApis() { + // 利用 ZStack 的反射工具扫描所有带 @MetadataImpact 注解的 APIMessage 子类 + BeanUtils.reflections.getTypesAnnotatedWith(MetadataImpact.class).forEach(clz -> { + if (APIMessage.class.isAssignableFrom(clz)) { + impactApiClasses.add((Class) clz); + logger.debug(String.format("detected @MetadataImpact API: %s", clz.getName())); + } + }); + } + + /** + * 通过 Resolver 链解析 API 消息关联的 vmInstanceUuid 列表。 + * + *

按注册顺序遍历 resolvers,使用第一个 supports() 返回 true 的 Resolver 进行解析。 + * 如果所有 Resolver 都不支持或返回空列表,则返回空。

+ */ + private List resolveVmUuids(APIMessage msg) { + for (VmUuidFromApiResolver resolver : resolvers) { + if (resolver.supports(msg)) { + List vmUuids = resolver.resolveVmUuids(msg); + if (vmUuids != null && !vmUuids.isEmpty()) { + return vmUuids; + } + } + } + logger.debug(String.format("no resolver could extract vmUuids from %s", msg.getClass().getName())); + return Collections.emptyList(); + } + + /** + * 提交元数据标脏。 + * + *

根据 {@link MetadataImpact.Impact} 决定 OP type:

+ *
    + *
  • {@code CONFIG} → storageStructureChange=false(OP type 1)
  • + *
  • {@code STORAGE} → storageStructureChange=true(OP type 2)
  • + *
+ * + * @param vmInstanceUuid 目标虚拟机 UUID + * @param impact 影响级别(来自 {@code @MetadataImpact} 注解) + */ + void submitMarkDirty(String vmInstanceUuid, MetadataImpact.Impact impact) { + boolean storageStructureChange = (impact == MetadataImpact.Impact.STORAGE); + logger.debug(String.format("[MetadataDirty] API succeeded, marking dirty " + + "for vm[uuid:%s], impact=%s, storageStructureChange=%s", + vmInstanceUuid, impact, storageStructureChange)); + dirtyMarker.markDirty(vmInstanceUuid, storageStructureChange); + } + + @Override + public boolean stop() { + stopCleanupTask(); + pendingApis.clear(); + return true; + } + + // ===================================================================== + // ManagementNodeReadyExtensionPoint + // ===================================================================== + + @Override + public void managementNodeReady() { + startCleanupTask(); + } + + // ===================================================================== + // pendingApis 超时清理 + // ===================================================================== + + /** + * 启动 pendingApis 超时清理周期任务。 + * + *

pendingApis 在 BeforeDeliveryMessageInterceptor 中写入,在 BeforePublishEventInterceptor + * 中消费。若 APIEvent 因 MN 崩溃、消息丢失等原因永远不发布,pendingApis 中的条目会泄漏。 + * 本任务定期清理超过 {@code vm.metadata.pendingApi.timeoutMinutes}(默认 45 分钟)的陈旧条目, + * 并为其调用 markDirty(保守策略:超时意味着 API 结果未知,保守标脏确保最终一致)。

+ */ + private synchronized void startCleanupTask() { + if (cleanupFuture != null) { + cleanupFuture.cancel(false); + } + // 每 5 分钟检查一次 + cleanupFuture = thdf.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } + + @Override + public long getInterval() { + return 5; + } + + @Override + public String getName() { + return "vm-metadata-pending-api-cleanup"; + } + + @Override + public void run() { + cleanupStalePendingApis(); + } + }); + logger.info("[MetadataInterceptor] pendingApis cleanup task started (check every 5min, " + + "timeout={}min)", VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT.value(Long.class)); + } + + private synchronized void stopCleanupTask() { + if (cleanupFuture != null) { + cleanupFuture.cancel(false); + cleanupFuture = null; + } + } + + /** + * 清理超时的 pendingApis 条目。 + * + *

超时条目执行保守 markDirty:API 结果未知,保守标脏确保元数据最终一致。 + * 使用 STORAGE 级别(storageStructureChange=true)以覆盖最坏情况。

+ */ + private void cleanupStalePendingApis() { + if (pendingApis.isEmpty()) { + return; + } + + long timeoutMs = VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT.value(Long.class) + * 60 * 1000; + long now = System.currentTimeMillis(); + int cleaned = 0; + + Iterator> it = pendingApis.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + MetadataImpactInfo info = entry.getValue(); + + if (now - info.timestamp > timeoutMs) { + it.remove(); + // 保守标脏:API 结果未知,假设成功,标脏确保最终一致 + for (String vmUuid : info.vmUuids) { + logger.warn("[MetadataInterceptor] pendingApi timeout: apiId={}, vm={}, " + + "age={}min. Conservative markDirty applied.", + entry.getKey(), vmUuid, (now - info.timestamp) / 60000); + dirtyMarker.markDirty(vmUuid, true); + } + cleaned++; + } + } + + if (cleaned > 0) { + logger.info("[MetadataInterceptor] cleaned {} stale pendingApi entries", cleaned); + } + } + + /** + * 缓存 API 投递时提取的元数据影响信息,供 Event 发布时匹配使用。 + */ + private static class MetadataImpactInfo { + final List vmUuids; + final MetadataImpact.Impact impact; + final boolean updateOnFailure; + final long timestamp; + + MetadataImpactInfo(List vmUuids, MetadataImpact.Impact impact, boolean updateOnFailure) { + this.vmUuids = vmUuids; + this.impact = impact; + this.updateOnFailure = updateOnFailure; + this.timestamp = System.currentTimeMillis(); + } + } +} \ No newline at end of file diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/VolumeBasedVmUuidFromApiResolver.java b/compute/src/main/java/org/zstack/compute/vm/metadata/VolumeBasedVmUuidFromApiResolver.java new file mode 100644 index 00000000000..b3f27a6eea9 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/VolumeBasedVmUuidFromApiResolver.java @@ -0,0 +1,66 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.db.SQL; +import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.VmUuidFromApiResolver; +import org.zstack.header.volume.VolumeMessage; + +import java.util.Collections; +import java.util.List; + +/** + * Volume 关联 VM UUID 解析器:从实现 {@link VolumeMessage} 接口的 API 消息中获取 volumeUuid, + * 查询 VolumeVO 得到关联的 vmInstanceUuid。 + * + *

覆盖快照、云盘挂载/卸载等涉及 Volume 但不直接携带 vmInstanceUuid 的 API。

+ * + *

排除条件

+ *

如果消息同时实现了 {@link VmInstanceMessage},则由 {@link DefaultVmUuidFromApiResolver} 处理, + * 本解析器不参与。

+ * + *

解析时机

+ *

在 API 执行前调用。对于 attach 场景,VolumeVO.vmInstanceUuid 可能尚未设置, + * 此时通过反射 fallback 到 msg.getVmInstanceUuid()(如果存在)。

+ */ +public class VolumeBasedVmUuidFromApiResolver implements VmUuidFromApiResolver { + + @Override + public boolean supports(APIMessage msg) { + // 同时实现 VmInstanceMessage 的由 DefaultResolver 处理 + return msg instanceof VolumeMessage && !(msg instanceof VmInstanceMessage); + } + + @Override + public List resolveVmUuids(APIMessage msg) { + String volumeUuid = ((VolumeMessage) msg).getVolumeUuid(); + if (volumeUuid == null) { + return Collections.emptyList(); + } + + // 查询 Volume → vmInstanceUuid + List vmUuids = SQL.New( + "SELECT v.vmInstanceUuid FROM VolumeVO v " + + "WHERE v.uuid = :uuid AND v.vmInstanceUuid IS NOT NULL", + String.class + ).param("uuid", volumeUuid).list(); + + if (!vmUuids.isEmpty()) { + return vmUuids; + } + + // Fallback:尝试通过反射获取 msg 上的 getVmInstanceUuid() + // 适用于 APIAttachDataVolumeToVmMsg 等同时携带 vmInstanceUuid 的消息 + try { + String vmUuid = (String) msg.getClass().getMethod("getVmInstanceUuid").invoke(msg); + if (vmUuid != null) { + return Collections.singletonList(vmUuid); + } + } catch (Exception ignored) { + // 无此方法,忽略 + } + + return Collections.emptyList(); + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIExportImageFromBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIExportImageFromBackupStorageMsg.java index 35edcd109ed..89b0a3edb52 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIExportImageFromBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIExportImageFromBackupStorageMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.message.DefaultTimeout; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -18,6 +19,7 @@ responseClass = APIExportImageFromBackupStorageEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIExportImageFromBackupStorageMsg extends APIMessage implements BackupStorageMessage, APIAuditor { @APIParam(resourceType = BackupStorageVO.class) private String backupStorageUuid; 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..9a0b9664816 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.MetadataImpact; import java.util.List; import java.util.concurrent.TimeUnit; @@ -44,6 +45,7 @@ responseClass = APIDeleteVolumeSnapshotEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 6) +@MetadataImpact(MetadataImpact.Impact.STORAGE) 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..e3827f38bbe 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.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(MetadataImpact.Impact.STORAGE) public class APIRevertVolumeFromSnapshotMsg extends APIMessage implements RevertVolumeSnapshotMessage, APIAuditor { /** * @desc volume snapshot 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..eec7f5cb291 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.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -19,6 +20,7 @@ responseClass = APIDeleteVolumeSnapshotGroupEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, 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/tag/APICreateSystemTagMsg.java b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java index a02a6001cf1..91744da003f 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.MetadataImpact; /** */ @@ -11,6 +12,7 @@ responseClass = APICreateSystemTagEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APICreateSystemTagMsg extends APIAbstractCreateTagMsg { public static APICreateSystemTagMsg __example__() { 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..c06abceb3aa 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.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteTagEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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..51c17f3b9aa 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.MetadataImpact; /** * Created by frank on 8/17/2015. @@ -14,6 +15,7 @@ isAction = true, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APIUpdateSystemTagMsg extends APIMessage { @APIParam(resourceType = SystemTagVO.class) private String 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..fafffe79f34 100644 --- a/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java @@ -11,6 +11,7 @@ responseClass = APIAttachVmNicToVmEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APIAttachVmNicToVmMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmNicVO.class) 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..ae9c67d5d6e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java @@ -16,6 +16,7 @@ method = HttpMethod.POST, responseClass = APIChangeVmNicNetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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..320b34050e9 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java @@ -22,6 +22,7 @@ responseClass = APIChangeVmNicStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/APIDeleteVmBootModeMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java index a2b73a69527..2edf2e39a84 100644 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java @@ -10,6 +10,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmBootModeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/APIDeleteVmHostnameMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java index 611c1c13da4..9cc7d73425d 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java @@ -13,6 +13,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmHostnameEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/APIDeleteVmNicMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmNicMsg.java index 19fc5f5e8bc..9c2af9e25c6 100644 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmNicMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmNicMsg.java @@ -10,6 +10,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmNicEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APIDeleteVmNicMsg extends APIDeleteMessage { @APIParam(resourceType = VmNicVO.class, successIfResourceNotExisting = true) 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..b71ab8e3d7a 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java @@ -12,6 +12,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmSshKeyEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APIDeleteVmSshKeyMsg extends APIMessage implements VmInstanceMessage { private String 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..58bb578a0f6 100755 --- a/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java @@ -14,6 +14,7 @@ method = HttpMethod.PUT, responseClass = APIRecoverVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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..e570c3e4914 100755 --- a/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java @@ -15,6 +15,7 @@ responseClass = APIReimageVmInstanceEvent.class, category = "vmInstance" ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) public class APIReimageVmInstanceMsg extends APIMessage implements VmInstanceMessage { public String getVmInstanceUuid() { return vmInstanceUuid; 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..0adf42f96d3 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java @@ -18,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APISetVmBootOrderEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/APIUpdateVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java index 60e9343ff38..d4b79951246 100755 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java @@ -15,6 +15,7 @@ isAction = true, responseClass = APIUpdateVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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/MetadataImpact.java b/header/src/main/java/org/zstack/header/vm/MetadataImpact.java new file mode 100644 index 00000000000..8c982b7687f --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/MetadataImpact.java @@ -0,0 +1,71 @@ +package org.zstack.header.vm; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标注 API 消息对虚拟机元数据的影响类型。 + * + *

opt-out 策略

+ *

不标注时默认行为等同于 {@link Impact#CONFIG}。 + * 明确不影响元数据的 API 应标注 {@link Impact#NONE}。

+ * + *

vmUuid 解析

+ *

不涉及 VM 的 API(如 APICreateZoneMsg)即使默认 CONFIG, + * 也不会触发元数据更新——因为 {@link VmUuidFromApiResolver} 无法解析出 vmUuid, + * 不会产生 {@link UpdateVmInstanceMetadataMsg}。

+ * + * @see VmUuidFromApiResolver + * @see UpdateVmInstanceMetadataMsg + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MetadataImpact { + + /** + * 影响类型。 + */ + Impact value(); + + /** + * API 失败时是否也需要更新元数据。 + * + *

默认 false:仅在 API 成功后触发元数据更新。 + * 设为 true 时,API 执行失败也会触发 markDirty。 + * 适用于 API 可能部分成功、需要同步最新状态的场景。

+ */ + boolean updateOnFailure() default false; + + /** + * API 对虚拟机元数据的影响类型枚举。 + */ + enum Impact { + /** + * 不影响虚拟机元数据,明确跳过。 + * + *

用于标注与 VM 无关或虽关联 VM 但不影响元数据内容的 API, + * 如 APIQueryVmInstanceMsg、APIGetVmConsoleAddressMsg 等。

+ */ + NONE, + + /** + * 影响虚拟机配置,触发元数据更新。 + * + *

如修改 CPU/内存、增删 SystemTag/ResourceConfig 等。 + * 这是未标注 {@link MetadataImpact} 注解时的默认行为。

+ */ + CONFIG, + + /** + * 影响存储结构,触发元数据更新。 + * + *

如存储迁移、快照操作、删除云盘等涉及存储结构变更的 API。 + * 在 sblk 场景下会设置 pending_op=2 以标记存储结构变更。

+ */ + STORAGE + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmUuidFromApiResolver.java b/header/src/main/java/org/zstack/header/vm/VmUuidFromApiResolver.java new file mode 100644 index 00000000000..1852f716827 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmUuidFromApiResolver.java @@ -0,0 +1,49 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIMessage; + +import java.util.List; + +/** + * 从 API 消息中解析关联的 vmInstanceUuid。 + * + *

用于非 VM 直接 API(如 Volume/Nic/快照 API)中提取关联的 VM UUID, + * 以便在 API 成功后触发对应 VM 的元数据更新。

+ * + *

实现类示例

+ *
    + *
  • VolumeToVmResolver:volumeUuid → vmInstanceUuid
  • + *
  • NicToVmResolver:vmNicUuid → vmInstanceUuid
  • + *
  • SnapshotToVmResolver:snapshotUuid → volumeUuid → vmInstanceUuid
  • + *
+ * + *

解析时机

+ *

Resolver 应在 API 执行前 预解析 vmUuid 并缓存在上下文中, + * 因为 API 执行后相关资源可能已被删除(如 APIDeleteVolumeMsg 执行后 VolumeVO 不存在)。

+ * + * @see MetadataImpact + * @see UpdateVmInstanceMetadataMsg + */ +public interface VmUuidFromApiResolver { + + /** + * 判断此 Resolver 是否能处理指定的 API 消息类型。 + * + * @param msg API 消息 + * @return true 表示此 Resolver 可以从该消息中解析 vmUuid + */ + boolean supports(APIMessage msg); + + /** + * 从 API 消息中解析出关联的 vmInstanceUuid 列表。 + * + *

可能返回空列表(如 volume 未挂载到任何 VM)。 + * 可能返回多个 UUID(如批量操作涉及多台 VM)。

+ * + *

此方法应在 API 执行前调用。

+ * + * @param msg API 消息 + * @return 关联的 vmInstanceUuid 列表,不为 null + */ + List resolveVmUuids(APIMessage msg); +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/cdrom/APIDeleteVmCdRomMsg.java b/header/src/main/java/org/zstack/header/vm/cdrom/APIDeleteVmCdRomMsg.java index a708c8fb2b3..3274219ca1d 100644 --- a/header/src/main/java/org/zstack/header/vm/cdrom/APIDeleteVmCdRomMsg.java +++ b/header/src/main/java/org/zstack/header/vm/cdrom/APIDeleteVmCdRomMsg.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.MetadataImpact; import org.zstack.header.vm.VmInstanceMessage; import org.zstack.header.vm.VmInstanceVO; @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmCdRomEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVmCdRomMsg extends APIDeleteMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = VmCdRomVO.class, successIfResourceNotExisting = true) 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..a2d3e9a4076 100755 --- a/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.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.MetadataImpact; import org.zstack.header.vm.VmInstanceVO; /** @@ -39,6 +40,7 @@ parameterName = "params", responseClass = APIAttachDataVolumeToVmEvent.class ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) public class APIAttachDataVolumeToVmMsg extends APIMessage implements VolumeMessage { /** * @desc vm uuid. see :ref:`VmInstanceInventory` 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..b211cd862c4 100644 --- a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefInventory; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.vm.MetadataImpact; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceVO; @@ -28,6 +29,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, updateOnFailure = true) public class APICreateVolumeSnapshotGroupMsg extends APICreateMessage implements VolumeMessage, CreateVolumeSnapshotGroupMessage, APIMultiAuditor { /** * @desc root 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..1b90420fb65 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.MetadataImpact; import java.util.List; @@ -42,6 +43,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteDataVolumeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) 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..4b93990dff3 100755 --- a/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; +import org.zstack.header.vm.MetadataImpact; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.rest.RestRequest; @@ -36,6 +37,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachDataVolumeFromVmEvent.class ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) 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/APIFlattenVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java index daeb56b44eb..817be91596b 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.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -17,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APIFlattenVolumeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) 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..ea62fcff23e 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.MetadataImpact; /** * Created by frank on 11/12/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIRecoverDataVolumeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.STORAGE) public class APIRecoverDataVolumeMsg 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..186519a2484 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 @@ -11,6 +11,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; import org.zstack.header.volume.VolumeVO; import java.util.concurrent.TimeUnit; @@ -25,6 +26,7 @@ isAction = true ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, 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..6aaa88a6f69 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java @@ -6,10 +6,12 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/resource-configurations/{category}/{name}/{resourceUuid}", method = HttpMethod.DELETE, responseClass = APIDeleteResourceConfigEvent.class) +@MetadataImpact(MetadataImpact.Impact.CONFIG) 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..4a4cf153b12 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java @@ -6,11 +6,13 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/resource-configurations/{category}/{name}/{resourceUuid}/actions", method = HttpMethod.PUT, isAction = true, responseClass = APIUpdateResourceConfigEvent.class) +@MetadataImpact(MetadataImpact.Impact.CONFIG) public class APIUpdateResourceConfigMsg extends APIMessage implements ResourceConfigMessage { @APIParam private String category; From d3d53a9e62165a701c70454819ad4d7bbcdd5a21 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:48:46 +0800 Subject: [PATCH 04/10] [vm-metadata]: dirty mark, poller and HA operations - VmMetadataDirtyMarker: CAS-based dirty record management - VmMetadataBuilder: assembles VmInstanceMetadataDTO from VM state - MetadataPathDriftDetector: path fingerprint drift detection - MetadataOrphanDetector: orphan metadata cleanup - MetadataStaleRecoveryTask: stale dirty record recovery - MetadataCascadeExtension: cascade delete support - MetadataPathSnapshotBuilder: snapshot chain path building - VmExpungeMetadataFlow: metadata cleanup on VM expunge - VmGlobalConfig: metadata feature toggle and poller configs - VmInstanceBase: metadata handling in VM lifecycle - AbstractVmInstance/VmInstanceUtils: metadata utility methods - GlobalConfig XML entries, Spring bean definitions - primaryStorage service config for metadata messages Resolves: ZSV-10000 Part: 02+02b --- .../zstack/compute/vm/AbstractVmInstance.java | 6 + .../compute/vm/VmExpungeMetadataFlow.java | 91 ++ .../org/zstack/compute/vm/VmGlobalConfig.java | 135 ++ .../org/zstack/compute/vm/VmInstanceBase.java | 114 +- .../zstack/compute/vm/VmInstanceUtils.java | 31 +- .../vm/metadata/MetadataCascadeExtension.java | 127 ++ .../vm/metadata/MetadataOrphanDetector.java | 287 ++++ .../metadata/MetadataPathDriftDetector.java | 214 +++ .../metadata/MetadataPathSnapshotBuilder.java | 88 ++ .../metadata/MetadataStaleRecoveryTask.java | 186 +++ .../vm/metadata/VmMetadataBuilder.java | 345 +++++ .../vm/metadata/VmMetadataDirtyMarker.java | 1201 +++++++++++++++++ conf/globalConfig/vm.xml | 64 + conf/serviceConfig/primaryStorage.xml | 3 + conf/springConfigXml/VmInstanceManager.xml | 57 +- 15 files changed, 2928 insertions(+), 21 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/MetadataCascadeExtension.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/MetadataOrphanDetector.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathDriftDetector.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathSnapshotBuilder.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/MetadataStaleRecoveryTask.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataBuilder.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataDirtyMarker.java diff --git a/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java b/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java index 6972470c964..9af3af26ee7 100755 --- a/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java +++ b/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java @@ -205,6 +205,12 @@ public abstract class AbstractVmInstance implements VmInstance { APIDestroyVmInstanceMsg.class.getName(), DestroyVmInstanceMsg.class.getName()); + // Registering state: only metadata-related reads, destroy (for cleanup/rollback), + // and ChangeVmMetaDataMsg (for state transitions during registration) are allowed. + allowedOperations.addState(VmInstanceState.Registering, + ChangeVmMetaDataMsg.class.getName(), + APIDestroyVmInstanceMsg.class.getName(), + DestroyVmInstanceMsg.class.getName()); stateChangeChecker.addState(VmInstanceStateEvent.unknown.toString(), VmInstanceState.Created.toString(), 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..946eeb3dce0 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java @@ -0,0 +1,91 @@ +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.db.Q; +import org.zstack.header.core.Completion; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.vm.MetadataStorageHandler; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.header.volume.VolumeType; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Map; + +/** + * VM 彻底删除(Expunge)时清理主存储上的元数据文件。 + * + *

设计要点(Part 02b §8.3):

+ *
    + *
  • 在 ExpungeVm 流程链中执行,位于 Root/Memory/Cache Volume 删除之后
  • + *
  • 通过根卷所在 PS 定位元数据位置
  • + *
  • best-effort:删除失败仅 WARN 日志,不阻塞 VM 物理清除
  • + *
  • dirty 行由 FK CASCADE 自动清理,本 Flow 不处理
  • + *
+ * + *

删除时机说明(Δ-5):元数据在 Expunge(物理删除)而非 Destroy(软删除) + * 阶段清理。Destroy 时 VM 可通过 Recover 恢复,过早删除会导致恢复后元数据丢失。

+ */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VmExpungeMetadataFlow extends NoRollbackFlow { + private static final CLogger logger = Utils.getLogger(VmExpungeMetadataFlow.class); + + @Autowired + private MetadataStorageHandler metadataStorageHandler; + + @Override + public void run(FlowTrigger trigger, Map data) { + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + final String vmUuid = spec.getVmInventory().getUuid(); + + // 功能开关检查:即使功能关闭,也尝试清理已有的元数据文件(best-effort) + // 不检查 VM_METADATA 开关——Expunge 是不可逆操作,应始终尝试清理残留 + + // 通过根卷查找 PS UUID + String rootVolumeUuid = spec.getVmInventory().getRootVolumeUuid(); + if (rootVolumeUuid == null) { + // VM 处于中间状态,无根卷,跳过 + logger.debug(String.format("[MetadataExpunge] vm[uuid:%s] has no root volume, skipping metadata cleanup", vmUuid)); + trigger.next(); + return; + } + + String psUuid = Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, rootVolumeUuid) + .select(VolumeVO_.primaryStorageUuid) + .findValue(); + + if (psUuid == null) { + // 根卷已被删除或无 PS 信息,跳过 + logger.debug(String.format("[MetadataExpunge] vm[uuid:%s] root volume[uuid:%s] has no primaryStorageUuid, " + + "skipping metadata cleanup", vmUuid, rootVolumeUuid)); + trigger.next(); + return; + } + + logger.info(String.format("[MetadataExpunge] deleting metadata for vm[uuid:%s] on ps[uuid:%s]", vmUuid, psUuid)); + + metadataStorageHandler.deleteMetadata(psUuid, vmUuid, new Completion(trigger) { + @Override + public void success() { + logger.info(String.format("[MetadataExpunge] metadata deleted for vm[uuid:%s] on ps[uuid:%s]", vmUuid, psUuid)); + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + // best-effort:失败不阻塞 VM 物理清除 + logger.warn(String.format("[MetadataExpunge] failed to delete metadata for vm[uuid:%s] on ps[uuid:%s], " + + "continuing expunge. Error: %s", vmUuid, psUuid, errorCode)); + trigger.next(); + } + }); + } +} 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..c0442dcf4c2 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,139 @@ public class VmGlobalConfig { @GlobalConfigValidation(validValues = {"None", "AuthenticAMD"}) @BindResourceConfig(value = {VmInstanceVO.class}) public static GlobalConfig VM_CPUID_VENDOR = new GlobalConfig(CATEGORY, "vm.cpuid.vendor"); + + @GlobalConfigValidation(numberGreaterThan = 1) + public static GlobalConfig GC_INTERVAL = new GlobalConfig(CATEGORY, "deletion.gcInterval"); + + @GlobalConfigValidation(validValues = {"true", "false"}) + public static GlobalConfig VM_METADATA = new GlobalConfig(CATEGORY, "vm.metadata"); + + @GlobalConfigDef(defaultValue = "5", type = Integer.class, + description = "Max concurrent metadata writes per primary storage per MN") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_PS_MAX_CONCURRENT = new GlobalConfig(CATEGORY, "vm.metadata.ps.maxConcurrent"); + + @GlobalConfigDef(defaultValue = "10", type = Integer.class, + description = "Max concurrent VM metadata updates globally per MN") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_GLOBAL_MAX_CONCURRENT = new GlobalConfig(CATEGORY, "vm.metadata.global.maxConcurrent"); + + @GlobalConfigDef(defaultValue = "10", type = Integer.class, + description = "Initial GC delay in seconds after API success") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_GC_INITIAL_DELAY_SEC = new GlobalConfig(CATEGORY, "vm.metadata.gc.initialDelaySec"); + + @GlobalConfigDef(defaultValue = "5", type = Integer.class, + description = "Max retry count before giving up metadata flush") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_MAX_RETRY = new GlobalConfig(CATEGORY, "vm.metadata.maxRetry"); + + @GlobalConfigDef(defaultValue = "5", type = Long.class, + description = "Dirty poller interval in seconds") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_DIRTY_POLL_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.dirty.pollIntervalSec"); + + @GlobalConfigDef(defaultValue = "20", type = Integer.class, + description = "Max dirty rows to claim per poller cycle") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_DIRTY_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.dirty.batchSize"); + + @GlobalConfigDef(defaultValue = "300", type = Long.class, + description = "Path fingerprint check interval in seconds") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_PATH_CHECK_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.pathCheck.intervalSec"); + + @GlobalConfigDef(defaultValue = "500", type = Integer.class, + description = "Path fingerprint check keyset pagination batch size") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_PATH_CHECK_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.pathCheck.batchSize"); + + @GlobalConfigDef(defaultValue = "600", type = Long.class, + description = "Delay in seconds before full refresh after upgrade, waiting for rolling upgrade to complete") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_UPGRADE_REFRESH_DELAY = new GlobalConfig(CATEGORY, "vm.metadata.upgrade.refreshDelaySec"); + + @GlobalConfigDef(defaultValue = "1000", type = Integer.class, + description = "Upgrade full refresh SQL batch size") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_UPGRADE_REFRESH_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.upgrade.refreshBatchSize"); + + @GlobalConfigDef(defaultValue = "5", type = Long.class, + description = "Delay in seconds after nodeLeft before takeover, reduces zombie MN race condition") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_NODE_LEFT_DELAY = new GlobalConfig(CATEGORY, "vm.metadata.nodeLeft.delaySec"); + + @GlobalConfigDef(defaultValue = "1800", type = Long.class, + description = "MetadataStaleRecoveryTask scan interval in seconds") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_STALE_RECOVERY_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.staleRecovery.intervalSec"); + + @GlobalConfigDef(defaultValue = "100", type = Integer.class, + description = "MetadataStaleRecoveryTask rows per scan batch") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_STALE_RECOVERY_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.staleRecovery.batchSize"); + + @GlobalConfigDef(defaultValue = "10", type = Integer.class, + description = "Max consecutive stale recovery cycles per VM before circuit-break") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_STALE_RECOVERY_MAX_CYCLES = new GlobalConfig(CATEGORY, "vm.metadata.staleRecovery.maxCycles"); + + @GlobalConfigDef(defaultValue = "45", type = Long.class, + description = "Pending API timeout cleanup threshold in minutes") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_PENDING_API_TIMEOUT = new GlobalConfig(CATEGORY, "vm.metadata.pendingApi.timeoutMinutes"); + + @GlobalConfigDef(defaultValue = "10", type = Integer.class, + description = "Exponential backoff base delay in seconds") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_RETRY_BASE_DELAY = new GlobalConfig(CATEGORY, "vm.metadata.retry.baseDelaySeconds"); + + @GlobalConfigDef(defaultValue = "10", type = Integer.class, + description = "Exponential backoff max exponent") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_RETRY_MAX_EXPONENT = new GlobalConfig(CATEGORY, "vm.metadata.retry.maxExponent"); + + @GlobalConfigDef(defaultValue = "200", type = Integer.class, + description = "Batch size per round when enabling metadata (false to true init)") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_INIT_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.init.batchSize"); + + @GlobalConfigDef(defaultValue = "5", type = Long.class, + description = "Delay in seconds between init batches to prevent IO storm") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_INIT_BATCH_DELAY = new GlobalConfig(CATEGORY, "vm.metadata.init.batchDelaySec"); + + @GlobalConfigDef(defaultValue = "3600", type = Long.class, + description = "Orphan metadata detection interval in seconds") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_ORPHAN_CHECK_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.orphanCheck.intervalSec"); + + @GlobalConfigDef(defaultValue = "15", type = Long.class, + description = "Zombie claim threshold in minutes: claimed dirty rows older than this are released") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_ZOMBIE_CLAIM_THRESHOLD = new GlobalConfig(CATEGORY, "vm.metadata.zombieClaim.thresholdMinutes"); + + @GlobalConfigDef(defaultValue = "30", type = Long.class, + description = "Stale claim threshold in minutes for background recovery task") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_STALE_CLAIM_THRESHOLD = new GlobalConfig(CATEGORY, "vm.metadata.staleClaim.thresholdMinutes"); + + @GlobalConfigDef(defaultValue = "10", type = Long.class, + description = "Inline stale claim takeover threshold in minutes for triggerFlushForVm hot path") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_TRIGGER_FLUSH_STALE = new GlobalConfig(CATEGORY, "vm.metadata.triggerFlush.staleMinutes"); + + @GlobalConfigDef(defaultValue = "3", type = Integer.class, + description = "Max retry count for deleteMetadata in ExpungeVmInstanceFlow") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_DELETE_MAX_RETRY = new GlobalConfig(CATEGORY, "vm.metadata.delete.maxRetry"); + + @GlobalConfigDef(defaultValue = "30", type = Long.class, + description = "Base delay in seconds for deleteMetadata retry backoff") + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_DELETE_BASE_DELAY = new GlobalConfig(CATEGORY, "vm.metadata.delete.baseDelaySec"); + + @GlobalConfigDef(defaultValue = "", type = String.class, + description = "Last completed upgrade refresh version, prevents duplicate triggers across MNs. Internal use only") + public static GlobalConfig VM_METADATA_LAST_REFRESH_VERSION = new GlobalConfig(CATEGORY, "vm.metadata.lastRefreshVersion"); } 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..7dcb5e6899a 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,15 @@ import org.zstack.header.message.*; import org.zstack.header.network.l3.*; import org.zstack.header.storage.primary.*; +import org.zstack.header.storage.snapshot.*; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.tag.SystemTagVO_; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import org.zstack.header.vm.*; import org.zstack.header.vm.ChangeVmMetaDataMsg.AtomicHostUuid; import org.zstack.header.vm.ChangeVmMetaDataMsg.AtomicVmState; @@ -66,23 +75,19 @@ import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.DnsUtils; import org.zstack.network.service.NetworkServiceManager; -import org.zstack.resourceconfig.ResourceConfig; -import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.resourceconfig.*; import org.zstack.tag.SystemTagCreator; import org.zstack.tag.SystemTagUtils; import org.zstack.tag.TagManager; -import org.zstack.utils.CollectionUtils; -import org.zstack.utils.ExceptionDSL; -import org.zstack.utils.ObjectUtils; -import org.zstack.utils.Utils; +import org.zstack.utils.*; import org.zstack.utils.function.ForEachFunction; import org.zstack.utils.function.Function; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; -import org.zstack.utils.network.NicIpAddressInfo; import org.zstack.utils.network.IPv6Constants; import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; +import org.zstack.utils.network.NicIpAddressInfo; import javax.persistence.PersistenceException; import javax.persistence.Tuple; @@ -90,6 +95,7 @@ import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static java.util.Arrays.asList; @@ -140,6 +146,10 @@ public class VmInstanceBase extends AbstractVmInstance { private VmInstanceResourceMetadataManager vidm; @Autowired private NetworkServiceManager nwServiceMgr; + @Autowired + private ResourceDestinationMaker destMaker; + @Autowired + private org.zstack.compute.vm.metadata.VmMetadataBuilder vmMetadataBuilder; protected VmInstanceVO self; protected VmInstanceVO originalCopy; @@ -533,6 +543,8 @@ protected void handleLocalMessage(Message msg) { handle((CancelFlattenVmInstanceMsg) msg); } else if (msg instanceof KvmReportVmShutdownEventMsg) { handle((KvmReportVmShutdownEventMsg) msg); + } else if (msg instanceof UpdateVmInstanceMetadataMsg) { + handle((UpdateVmInstanceMetadataMsg) msg); } else { VmInstanceBaseExtensionFactory ext = vmMgr.getVmInstanceBaseExtensionFactory(msg); if (ext != null) { @@ -3184,6 +3196,17 @@ protected List getImageCandidatesForVm(ImageMediaType type) { } protected void handleApiMessage(APIMessage msg) { + // Guard: reject most API operations while VM is in Registering state (§3.5) + if (self != null && self.getState() == VmInstanceState.Registering) { + // Only allow Destroy and query/read operations during registration + if (!(msg instanceof APIDestroyVmInstanceMsg)) { + bus.replyErrorByMessageType((Message) msg, + operr("vm[uuid:%s] is in Registering state, operation[%s] is not allowed", + self.getUuid(), msg.getMessageName())); + return; + } + } + if (msg instanceof APIStopVmInstanceMsg) { handle((APIStopVmInstanceMsg) msg); } else if (msg instanceof APIRebootVmInstanceMsg) { @@ -9369,5 +9392,80 @@ public void run(MessageReply reply) { } }); } -} + /** + * 处理元数据更新消息。 + * + *

通过 ChainTask 确保同一 VM 的元数据更新串行执行。 + * 该消息由 VmMetadataDirtyMarker 发送到本地 VM 服务, + * 内部从 DB 全量构建 metadata payload 后写入主存储。

+ * + *

失败路径直接返回错误 reply,由 VmMetadataDirtyMarker 的 + * onFlushFailure() 统一处理重试和指数退避。

+ */ + private void handle(UpdateVmInstanceMetadataMsg msg) { + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return String.format("handle-update-vm-%s-metadata", msg.getUuid()); + } + + @Override + public void run(SyncTaskChain chain) { + doHandleUpdateVmInstanceMetadata(msg); + chain.next(); + } + + @Override + public String getName() { + return String.format("handle-update-vm-%s-metadata-task", msg.getUuid()); + } + }); + } + + private void doHandleUpdateVmInstanceMetadata(UpdateVmInstanceMetadataMsg msg) { + // 1. 构建 payload(通过 VmMetadataBuilder 在 @Transactional(readOnly=true) 事务内完成) + String metadata = vmMetadataBuilder.buildVmInstanceMetadata(msg.getUuid()); + + // 2. Payload 大小保护 + int payloadSize = metadata.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (payloadSize > org.zstack.compute.vm.metadata.VmMetadataBuilder.REJECT_THRESHOLD) { + logger.error(String.format("metadata payload too large: %d bytes for vm[uuid:%s], rejecting", + payloadSize, msg.getUuid())); + MessageReply reply = new MessageReply(); + reply.setError(Platform.operr("metadata payload too large (%d bytes, limit %d) for vm[uuid=%s]", + payloadSize, org.zstack.compute.vm.metadata.VmMetadataBuilder.REJECT_THRESHOLD, msg.getUuid())); + bus.reply(msg, reply); + return; + } + if (payloadSize > org.zstack.compute.vm.metadata.VmMetadataBuilder.WARN_THRESHOLD) { + logger.warn(String.format("metadata payload large: %d bytes for vm[uuid:%s]", + payloadSize, msg.getUuid())); + } + + // 3. 发送到主存储 + Tuple tuple = Q.New(VolumeVO.class).select(VolumeVO_.primaryStorageUuid, VolumeVO_.uuid) + .eq(VolumeVO_.vmInstanceUuid, msg.getUuid()).eq(VolumeVO_.type, VolumeType.Root).findTuple(); + String primaryStorageUuid = tuple.get(0, String.class); + String rootVolumeUuid = tuple.get(1, String.class); + + UpdateVmInstanceMetadataOnPrimaryStorageMsg umsg = new UpdateVmInstanceMetadataOnPrimaryStorageMsg(); + umsg.setMetadata(metadata); + umsg.setPrimaryStorageUuid(primaryStorageUuid); + umsg.setRootVolumeUuid(rootVolumeUuid); + umsg.setStorageStructureChange(msg.isStorageStructureChange()); + bus.makeLocalServiceId(umsg, PrimaryStorageConstant.SERVICE_ID); + bus.send(umsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + + if (!r.isSuccess()) { + reply.setError(Platform.operr("failed to update vm[uuid=%s] metadata on primary storage", + msg.getUuid()).withCause(r.getError())); + } + bus.reply(msg, reply); + } + }); + } +} 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..1f916560f58 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java @@ -2,22 +2,29 @@ import org.apache.commons.collections.CollectionUtils; import org.zstack.core.Platform; +import org.zstack.core.db.Q; import org.zstack.header.configuration.InstanceOfferingInventory; import org.zstack.header.errorcode.OperationFailureException; -import org.zstack.header.vm.APIChangeInstanceOfferingMsg; -import org.zstack.header.vm.APICreateVmInstanceMsg; -import org.zstack.header.vm.CreateVmInstanceMsg; -import org.zstack.header.vm.DiskAO; -import org.zstack.header.vm.UpdateVmInstanceMsg; -import org.zstack.header.vm.UpdateVmInstanceSpec; -import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.tag.SystemTagVO_; +import org.zstack.header.vm.*; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.resourceconfig.ResourceConfigVO; +import org.zstack.resourceconfig.ResourceConfigVO_; import org.zstack.tag.SystemTagUtils; +import org.zstack.utils.function.ForEachFunction; +import org.zstack.utils.gson.JSONObjectUtil; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; +import java.util.stream.Collectors; import static java.util.Objects.requireNonNull; import static org.zstack.compute.vm.VmSystemTags.PRIMARY_STORAGE_UUID_FOR_DATA_VOLUME; diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataCascadeExtension.java new file mode 100644 index 00000000000..29c3c82054a --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataCascadeExtension.java @@ -0,0 +1,127 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.core.cascade.AbstractAsyncCascadeExtension; +import org.zstack.core.cascade.CascadeAction; +import org.zstack.core.cascade.CascadeConstant; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.header.core.Completion; +import org.zstack.header.volume.VolumeDeletionStruct; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 监听 Volume 级联删除事件,为受影响的 VM 触发元数据标脏。 + * + *

设计背景

+ *

{@code @MetadataImpact} 注解仅标注在 {@code APIMessage} 子类上, + * 通过 {@link VmMetadataUpdateInterceptor} 自动触发标脏。 + * 但系统中存在不经过 API 拦截器的级联删除操作也会修改 VM 存储拓扑, + * 例如:删除 PrimaryStorage → 级联删除 Volume → VM 失去数据卷。 + * 本扩展在级联清理阶段({@code DELETION_CLEANUP_CODE})捕获这些事件, + * 为受影响的 VM 调用 markDirty。

+ * + *

Cascade 图位置

+ *
+ *   ... → PrimaryStorageVO → VolumeVO → VmInstanceMetadata (本扩展)
+ *                                     → VolumeSnapshotVO → ...
+ * 
+ * + *

两道防线

+ *
    + *
  1. 本扩展 + {@code @MetadataImpact} 拦截器覆盖大部分场景
  2. + *
  3. 健康巡检兜底:周期全量比对 DB vs 存储元数据
  4. + *
+ */ +public class MetadataCascadeExtension extends AbstractAsyncCascadeExtension { + private static final CLogger logger = Utils.getLogger(MetadataCascadeExtension.class); + + private static final String NAME = "VmInstanceMetadata"; + + @Autowired + private VmMetadataDirtyMarker dirtyMarker; + + @Autowired + private DatabaseFacade dbf; + + @Override + public void asyncCascade(CascadeAction action, Completion completion) { + if (!action.isActionCode(CascadeConstant.DELETION_CLEANUP_CODE)) { + completion.success(); + return; + } + + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + completion.success(); + return; + } + + List vmUuids = extractAffectedVmUuids(action); + if (vmUuids.isEmpty()) { + completion.success(); + return; + } + + for (String vmUuid : vmUuids) { + // 检查 VM 是否仍然存在(级联删除 VM 时不需要更新元数据) + if (dbf.isExist(vmUuid, VmInstanceVO.class)) { + logger.debug(String.format("[MetadataCascade] volume cascade cleanup affected " + + "vm[uuid:%s], marking dirty for metadata update", vmUuid)); + // 级联删除 Volume 属于存储结构变更(STORAGE → OP type 2) + dirtyMarker.markDirty(vmUuid, true); + } + } + + completion.success(); + } + + /** + * 从 CascadeAction 上下文中提取受影响的 VM UUID 列表。 + * + *

当前支持的 parentIssuer:

+ *
    + *
  • {@code VolumeVO} → 从 {@link VolumeDeletionStruct} 中获取 vmInstanceUuid
  • + *
+ */ + private List extractAffectedVmUuids(CascadeAction action) { + if (VolumeVO.class.getSimpleName().equals(action.getParentIssuer())) { + List structs = action.getParentIssuerContext(); + if (structs == null || structs.isEmpty()) { + return Collections.emptyList(); + } + + return structs.stream() + .map(s -> s.getInventory().getVmInstanceUuid()) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + + @Override + public List getEdgeNames() { + return Arrays.asList(VolumeVO.class.getSimpleName()); + } + + @Override + public String getCascadeResourceName() { + return NAME; + } + + @Override + public CascadeAction createActionForChildResource(CascadeAction action) { + // 叶子节点,不向下传播级联 + return null; + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataOrphanDetector.java b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataOrphanDetector.java new file mode 100644 index 00000000000..2f57f0c9f83 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataOrphanDetector.java @@ -0,0 +1,287 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +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.managementnode.ManagementNodeReadyExtensionPoint; +import org.zstack.header.storage.primary.PrimaryStorageState; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; +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.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 孤儿元数据检测器:周期性扫描各 PS 上残留的元数据条目,检测并报告孤儿。 + * + *

设计背景(Part 01c §1.3, Part 02b C-02B-14)

+ *

VM 删除时元数据同步清理可能因 IO 错误失败(3 次重试后放弃), + * 或 VM 创建失败导致残留。本检测器作为安全网,周期性地扫描每个 + * 支持元数据的 PS,比对存储侧 vmUuid 列表与 DB 中实际存在的 VM, + * 发现孤儿后仅记录日志告警,不执行自动删除

+ * + *

孤儿判定条件

+ *
    + *
  • 存储侧有元数据但 DB 中 VM 不存在(已彻底 Expunge)
  • + *
  • 存储侧有元数据但该 VM 的 Root Volume 不在此 PS 上(迁移残留)
  • + *
+ * + *

约束

+ *
    + *
  • C-02B-14: 仅报告不自动删除,避免与进行中的存储迁移竞态导致误删
  • + *
  • 仅扫描 {@code PrimaryStorageState.Enabled} 的 PS
  • + *
  • 依赖 {@code MetadataStorageHandler.scanMetadataVmUuids()} — 当前为骨架实现
  • + *
+ * + *

TODO

+ *

{@code MetadataStorageHandler} 接口及其实现(SblkMetadataStorageHandler, + * LocalNfsMetadataStorageHandler)尚未创建。本类在 scanMetadataVmUuids 可用后 + * 需取消 TODO 标记并完成 Agent 调用接入。

+ */ +public class MetadataOrphanDetector implements Component, ManagementNodeReadyExtensionPoint { + private static final CLogger logger = Utils.getLogger(MetadataOrphanDetector.class); + + // TODO: 待 MetadataStorageHandler 接口创建后注入 + // @Autowired + // private List metadataStorageHandlers; + + @Autowired + private ThreadFacade thdf; + + private Future taskFuture; + + // ===================================================================== + // 支持元数据的 PS 类型(与 MetadataStorageHandler.isMetadataSupported 对齐) + // ===================================================================== + + /** + * 当前支持元数据的存储类型。 + * 待 MetadataStorageHandler 接口就绪后,应通过 handler.isMetadataSupported() 动态判断。 + */ + private static final List SUPPORTED_PS_TYPES = List.of( + "SharedBlock", + "LocalStorage", + "NFS" + ); + + // ===================================================================== + // Component 生命周期 + // ===================================================================== + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + stopTask(); + return true; + } + + // ===================================================================== + // ManagementNodeReadyExtensionPoint + // ===================================================================== + + @Override + public void managementNodeReady() { + startTask(); + } + + // ===================================================================== + // 任务管理 + // ===================================================================== + + private synchronized void startTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + } + taskFuture = thdf.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_ORPHAN_CHECK_INTERVAL.value(Long.class); + } + + @Override + public String getName() { + return "vm-metadata-orphan-detector"; + } + + @Override + public void run() { + detectOrphans(); + } + }); + logger.info("[MetadataOrphanDetector] task started (interval={}s)", + VmGlobalConfig.VM_METADATA_ORPHAN_CHECK_INTERVAL.value(Long.class)); + } + + private synchronized void stopTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + taskFuture = null; + logger.info("[MetadataOrphanDetector] task stopped"); + } + } + + // ===================================================================== + // 核心检测逻辑 + // ===================================================================== + + /** + * 扫描所有支持元数据的已启用 PS,对比存储侧 vmUuid 列表与 DB 状态, + * 检测并报告孤儿元数据。 + * + *

C-02B-14: 仅报告(WARN 日志),不执行 deleteMetadata。

+ */ + private void detectOrphans() { + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + // 查询所有已启用且支持元数据的 PS + List psList = Q.New(PrimaryStorageVO.class) + .eq(PrimaryStorageVO_.state, PrimaryStorageState.Enabled) + .in(PrimaryStorageVO_.type, SUPPORTED_PS_TYPES) + .list(); + + if (psList.isEmpty()) { + return; + } + + int totalOrphans = 0; + + for (PrimaryStorageVO ps : psList) { + try { + int orphanCount = scanPsForOrphans(ps); + totalOrphans += orphanCount; + } catch (Exception e) { + logger.warn("[MetadataOrphanDetector] failed to scan PS [{}] (type={}): {}", + ps.getUuid(), ps.getType(), e.getMessage()); + } + } + + if (totalOrphans > 0) { + logger.warn("[MetadataOrphanDetector] scan complete: {} orphan(s) detected across {} PS(es). " + + "Use APICleanupVmInstanceMetadataMsg to clean up manually.", totalOrphans, psList.size()); + } + } + + /** + * 扫描单个 PS 上的元数据条目,识别孤儿。 + * + *

TODO: 当前为骨架实现。待 MetadataStorageHandler.scanMetadataVmUuids() 接口 + * 就绪后,替换下方 TODO 块为实际 agent 调用。

+ * + * @param ps 目标 PrimaryStorageVO + * @return 检测到的孤儿数量 + */ + private int scanPsForOrphans(PrimaryStorageVO ps) { + String psUuid = ps.getUuid(); + String psType = ps.getType(); + + // =================================================================== + // TODO: 替换为 MetadataStorageHandler.scanMetadataVmUuids(psUuid) 调用 + // + // 预期调用模式: + // MetadataStorageHandler handler = findHandler(psType); + // handler.scanMetadataVmUuids(psUuid, new ReturnValueCompletion>(null) { + // @Override + // public void success(List entries) { + // int orphans = checkOrphanEntries(psUuid, entries); + // // ... + // } + // @Override + // public void fail(ErrorCode errorCode) { + // logger.warn("scan failed for PS [{}]: {}", psUuid, errorCode); + // } + // }); + // + // VmMetadataEntry 结构(Part 01c §1.3): + // - vmUuid: String + // - hostUuid: String (nullable, 仅 LocalStorage 场景有值) + // =================================================================== + + logger.debug("[MetadataOrphanDetector] scanning PS [{}] (type={}) — skipped: " + + "MetadataStorageHandler not yet implemented", psUuid, psType); + return 0; + } + + /** + * 检查从 agent 扫描返回的 vmUuid 列表,识别孤儿。 + * + *

孤儿条件:

+ *
    + *
  1. vmUuid 在 VmInstanceVO 中不存在(已彻底 Expunge)
  2. + *
  3. vmUuid 存在但其 Root Volume 的 primaryStorageUuid 不等于当前 PS(迁移残留)
  4. + *
+ * + *

C-02B-14: 仅报告(WARN 日志),不执行自动删除。

+ * + * @param psUuid 当前扫描的 PS UUID + * @param metadataVmUuids agent 扫描返回的 vmUuid 列表 + * @return 检测到的孤儿数量 + */ + int checkOrphanEntries(String psUuid, List metadataVmUuids) { + if (metadataVmUuids == null || metadataVmUuids.isEmpty()) { + return 0; + } + + // 批量查询 DB 中存在的 VM UUIDs + List existingVmUuids = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.uuid) + .in(VmInstanceVO_.uuid, metadataVmUuids) + .listValues(); + + int orphanCount = 0; + + // 类型 1: VM 不存在(已 Expunge) + List expungedVmUuids = metadataVmUuids.stream() + .filter(uuid -> !existingVmUuids.contains(uuid)) + .collect(Collectors.toList()); + + for (String vmUuid : expungedVmUuids) { + logger.warn("[MetadataOrphanDetector] orphan detected: VM [{}] on PS [{}] — " + + "VM no longer exists in DB (expunged)", vmUuid, psUuid); + orphanCount++; + } + + // 类型 2: VM 存在但 Root Volume 不在此 PS 上(迁移残留) + if (!existingVmUuids.isEmpty()) { + // 查询这些 VM 的 Root Volume 所在 PS + List rootVolumes = Q.New(VolumeVO.class) + .eq(VolumeVO_.type, VolumeType.Root) + .in(VolumeVO_.vmInstanceUuid, existingVmUuids) + .list(); + + for (VolumeVO rootVol : rootVolumes) { + if (rootVol.getPrimaryStorageUuid() != null + && !rootVol.getPrimaryStorageUuid().equals(psUuid)) { + logger.warn("[MetadataOrphanDetector] orphan detected: VM [{}] on PS [{}] — " + + "root volume is on PS [{}] (migration residue)", + rootVol.getVmInstanceUuid(), psUuid, rootVol.getPrimaryStorageUuid()); + orphanCount++; + } + } + } + + return orphanCount; + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathDriftDetector.java b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathDriftDetector.java new file mode 100644 index 00000000000..629f096b464 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathDriftDetector.java @@ -0,0 +1,214 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; +import org.zstack.core.db.SimpleQuery; +import org.zstack.core.thread.PeriodicTask; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.header.Component; +import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +import org.zstack.header.vm.VmMetadataPathFingerprintVO; +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.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 路径指纹巡检任务:周期性检测 VM 存储拓扑是否发生漂移。 + * + *

设计背景(Part 02b §8.2)

+ *

当存储拓扑变更绕过了 {@code @MetadataImpact} 拦截器(例如底层存储迁移、 + * 手动数据库修改等),dirty mark 不会被触发,导致元数据与实际拓扑不一致。 + * 本巡检任务作为安全网,定期比对每个 VM 的当前路径快照与上次刷写时记录的 + * 路径指纹,发现漂移则调用 markDirty 触发重新刷写。

+ * + *

巡检策略

+ *
    + *
  • C-02B-3: 禁止 listAll,必须使用 keyset 分页(vmInstanceUuid > lastUuid)
  • + *
  • 零存储 I/O:纯 DB 查询比对,不涉及 agent 调用
  • + *
  • pathSnapshot JSON 格式与 {@link MetadataPathSnapshotBuilder#buildPathJson} 保持一致
  • + *
  • 仅在上次成功刷写过的 VM(有指纹记录)上进行巡检,从未刷写过的 VM 自动跳过
  • + *
+ */ +public class MetadataPathDriftDetector implements Component, ManagementNodeReadyExtensionPoint { + private static final CLogger logger = Utils.getLogger(MetadataPathDriftDetector.class); + + @Autowired + private VmMetadataDirtyMarker dirtyMarker; + + @Autowired + private ThreadFacade thdf; + + private Future taskFuture; + + // ===================================================================== + // Component 生命周期 + // ===================================================================== + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + stopTask(); + return true; + } + + // ===================================================================== + // ManagementNodeReadyExtensionPoint + // ===================================================================== + + @Override + public void managementNodeReady() { + startTask(); + } + + // ===================================================================== + // 任务管理 + // ===================================================================== + + private synchronized void startTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + } + taskFuture = thdf.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_PATH_CHECK_INTERVAL.value(Long.class); + } + + @Override + public String getName() { + return "vm-metadata-path-drift-detector"; + } + + @Override + public void run() { + detectPathDrift(); + } + }); + logger.info("[MetadataPathDrift] task started (interval={}s)", + VmGlobalConfig.VM_METADATA_PATH_CHECK_INTERVAL.value(Long.class)); + } + + private synchronized void stopTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + taskFuture = null; + logger.info("[MetadataPathDrift] task stopped"); + } + } + + // ===================================================================== + // 核心巡检逻辑 + // ===================================================================== + + /** + * 使用 keyset 分页遍历所有指纹记录,比对当前路径快照。 + * + *

C-02B-3: 禁止 listAll,使用 {@code vmInstanceUuid > lastUuid} 分页。 + * 因 PK 为 vmInstanceUuid(非自增 id),keyset 分页天然适用。

+ */ + private void detectPathDrift() { + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + int batchSize = VmGlobalConfig.VM_METADATA_PATH_CHECK_BATCH_SIZE.value(Integer.class); + String lastUuid = ""; + int driftCount = 0; + int totalChecked = 0; + + while (true) { + List batch = SQL.New( + "SELECT fp FROM VmMetadataPathFingerprintVO fp " + + "WHERE fp.vmInstanceUuid > :lastUuid " + + "ORDER BY fp.vmInstanceUuid ASC", + VmMetadataPathFingerprintVO.class) + .param("lastUuid", lastUuid) + .limit(batchSize) + .list(); + + if (batch.isEmpty()) { + break; + } + + for (VmMetadataPathFingerprintVO fp : batch) { + String vmUuid = fp.getVmInstanceUuid(); + String currentSnapshot = buildCurrentPathSnapshot(vmUuid); + + // pathSnapshot 可能为 null(简化实现阶段的历史记录) + String recordedSnapshot = fp.getPathSnapshot(); + if (recordedSnapshot == null) { + // 无历史指纹,跳过(等待下次刷写补充) + continue; + } + + if (!recordedSnapshot.equals(currentSnapshot)) { + logger.warn("[MetadataPathDrift] drift detected for VM [{}], " + + "recorded: {}, current: {}", vmUuid, recordedSnapshot, currentSnapshot); + dirtyMarker.markDirty(vmUuid); + driftCount++; + } + totalChecked++; + } + + lastUuid = batch.get(batch.size() - 1).getVmInstanceUuid(); + } + + if (driftCount > 0) { + logger.info("[MetadataPathDrift] scan complete: checked={}, driftDetected={}", + totalChecked, driftCount); + } + } + + /** + * 构建 VM 的当前路径快照 JSON。 + * + *

与 {@link MetadataPathSnapshotBuilder#buildPathJson} 使用完全相同的逻辑, + * 确保比对结果一致:

+ *
    + *
  • volumes: 按 uuid ASC 排序
  • + *
  • snapshots: 按 uuid ASC 排序,仅包含 volumes 关联的快照
  • + *
  • JSON 字段声明顺序固定(uuid, installPath),Gson 按声明顺序输出
  • + *
+ */ + private String buildCurrentPathSnapshot(String vmUuid) { + List volumes = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .orderBy(VolumeVO_.uuid, SimpleQuery.Od.ASC) + .list(); + + List snapshots; + if (volumes.isEmpty()) { + snapshots = new ArrayList<>(); + } else { + List volumeUuids = volumes.stream() + .map(VolumeVO::getUuid) + .collect(Collectors.toList()); + snapshots = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.volumeUuid, volumeUuids) + .orderBy(VolumeSnapshotVO_.uuid, SimpleQuery.Od.ASC) + .list(); + } + + return MetadataPathSnapshotBuilder.buildPathJson(volumes, snapshots); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathSnapshotBuilder.java b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathSnapshotBuilder.java new file mode 100644 index 00000000000..03569c79d02 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataPathSnapshotBuilder.java @@ -0,0 +1,88 @@ +package org.zstack.compute.vm.metadata; + +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.volume.VolumeVO; +import org.zstack.utils.gson.JSONObjectUtil; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 构建 VM 存储路径指纹 JSON 的共享工具类。 + * + *

同时被 {@link VmMetadataDirtyMarker#savePathFingerprint} 和 + * {@link MetadataPathDriftDetector#buildCurrentPathSnapshot} 使用, + * 确保写入时记录的指纹与巡检时构建的指纹使用完全相同的逻辑。

+ * + *

JSON 格式

+ *
+ * {
+ *   "volumes": [
+ *     {"uuid": "vol-aaa", "installPath": "/dev/vg/vol-aaa"},
+ *     {"uuid": "vol-bbb", "installPath": "/dev/vg/vol-bbb"}
+ *   ],
+ *   "snapshots": [
+ *     {"uuid": "sp-001", "installPath": "/dev/vg/sp-001"},
+ *     {"uuid": "sp-002", "installPath": "/dev/vg/sp-002"}
+ *   ]
+ * }
+ * 
+ * + *

确定性保证

+ *
    + *
  • 列表层面:按 uuid ASC 排序(调用方负责传入已排序列表)
  • + *
  • 字段层面:Gson 按 Java 字段声明顺序输出(uuid 在前, installPath 在后)
  • + *
+ */ +public class MetadataPathSnapshotBuilder { + + /** + * 构建路径指纹 JSON。 + * + * @param volumes 已按 uuid ASC 排序的 VolumeVO 列表 + * @param snapshots 已按 uuid ASC 排序的 VolumeSnapshotVO 列表 + * @return 确定性 JSON 字符串 + */ + public static String buildPathJson(List volumes, List snapshots) { + List volumeEntries = volumes.stream() + .map(v -> new PathEntry(v.getUuid(), v.getInstallPath())) + .collect(Collectors.toList()); + + List snapshotEntries = snapshots.stream() + .map(s -> new PathEntry(s.getUuid(), s.getPrimaryStorageInstallPath())) + .collect(Collectors.toList()); + + PathSnapshot snapshot = new PathSnapshot(volumeEntries, snapshotEntries); + return JSONObjectUtil.toJsonString(snapshot); + } + + /** + * 路径快照顶层结构。 + * + *

Gson 按字段声明顺序序列化:volumes → snapshots。

+ */ + private static class PathSnapshot { + final List volumes; + final List snapshots; + + PathSnapshot(List volumes, List snapshots) { + this.volumes = volumes; + this.snapshots = snapshots; + } + } + + /** + * 单条路径条目。 + * + *

Gson 按字段声明顺序序列化:uuid → installPath。

+ */ + private static class PathEntry { + final String uuid; + final String installPath; + + PathEntry(String uuid, String installPath) { + this.uuid = uuid; + this.installPath = installPath; + } + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataStaleRecoveryTask.java b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataStaleRecoveryTask.java new file mode 100644 index 00000000000..af4a89ca023 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/MetadataStaleRecoveryTask.java @@ -0,0 +1,186 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.core.db.SQL; +import org.zstack.core.thread.PeriodicTask; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.header.Component; +import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint; +import org.zstack.header.vm.VmMetadataPathFingerprintVO; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Stale 恢复任务:为重试耗尽(lastFlushFailed=true)的 VM 重新入队 markDirty。 + * + *

设计背景(Part 02 §4.8)

+ *

当 dirty 行因重试耗尽被删除后,低频 VM(长期无 {@code @MetadataImpact} API)将失去 + * 自愈机会。本任务作为独立低频扫描器,周期性地将这些 VM 重新标脏,给予全新重试机会。

+ * + *

慢速重试闭环

+ *
+ *   lastFlushFailed=true
+ *     → StaleRecoveryTask markDirty(retryCount=0)
+ *       → Poller 5 次重试
+ *         → 若仍失败 → lastFlushFailed=true → 30min 后再来
+ *         → 若成功 → 正常完成
+ * 
+ * + *

熔断机制(Q27)

+ *

当 PS 长期不可达时,staleRecoveryCount 累加。达到上限(默认 10 ≈ 5 小时)后 + * 停止自动恢复,记 WARN 日志提示管理员手动触发。

+ * + *

约束

+ *
    + *
  • C-SR-06: markDirty 使用 retryCount=0(全新起点),不继承历史退避
  • + *
  • C-02B-8: lastFlushFailed 仅在 markDirty 成功时重置为 false
  • + *
  • DP-03: 先验证 markDirty 返回值,仅在成功时清除 lastFlushFailed
  • + *
+ */ +public class MetadataStaleRecoveryTask implements Component, ManagementNodeReadyExtensionPoint { + private static final CLogger logger = Utils.getLogger(MetadataStaleRecoveryTask.class); + + @Autowired + private VmMetadataDirtyMarker dirtyMarker; + + @Autowired + private ThreadFacade thdf; + + private Future taskFuture; + + // ===================================================================== + // Component 生命周期 + // ===================================================================== + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + stopTask(); + return true; + } + + // ===================================================================== + // ManagementNodeReadyExtensionPoint + // ===================================================================== + + @Override + public void managementNodeReady() { + startTask(); + } + + // ===================================================================== + // 任务管理 + // ===================================================================== + + private synchronized void startTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + } + taskFuture = thdf.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_STALE_RECOVERY_INTERVAL.value(Long.class); + } + + @Override + public String getName() { + return "vm-metadata-stale-recovery"; + } + + @Override + public void run() { + recoverStaleVms(); + } + }); + logger.info("[MetadataStaleRecovery] task started (interval={}s)", + VmGlobalConfig.VM_METADATA_STALE_RECOVERY_INTERVAL.value(Long.class)); + } + + private synchronized void stopTask() { + if (taskFuture != null) { + taskFuture.cancel(false); + taskFuture = null; + logger.info("[MetadataStaleRecovery] task stopped"); + } + } + + // ===================================================================== + // 核心逻辑 + // ===================================================================== + + private void recoverStaleVms() { + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + int batchSize = VmGlobalConfig.VM_METADATA_STALE_RECOVERY_BATCH_SIZE.value(Integer.class); + int maxCycles = VmGlobalConfig.VM_METADATA_STALE_RECOVERY_MAX_CYCLES.value(Integer.class); + + // 查找所有 lastFlushFailed=true 的指纹记录 + List staleVms = SQL.New( + "SELECT fp FROM VmMetadataPathFingerprintVO fp WHERE fp.lastFlushFailed = 1", + VmMetadataPathFingerprintVO.class) + .limit(batchSize) + .list(); + + if (staleVms.isEmpty()) { + return; + } + + int requeued = 0; + int circuitBroken = 0; + + for (VmMetadataPathFingerprintVO fp : staleVms) { + String vmUuid = fp.getVmInstanceUuid(); + + // Q27 熔断检查:staleRecoveryCount 达到上限 → 停止自动恢复 + if (fp.getStaleRecoveryCount() >= maxCycles) { + // 置 lastFlushFailed=false 停止后续扫描 + SQL.New("UPDATE VmMetadataPathFingerprintVO " + + "SET lastFlushFailed = 0 WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmUuid) + .execute(); + + logger.warn("VM [{}] metadata stale recovery exceeded {} cycles, entering permanent-stale. " + + "Use APIUpdateVmMetadataMsg to manually trigger.", vmUuid, maxCycles); + circuitBroken++; + continue; + } + + // C-SR-06: markDirty 使用 retryCount=0(全新起点,由 markDirty 内部 INSERT IGNORE 保证) + // DP-03: 先验证 markDirty 返回值 + boolean markSuccess = dirtyMarker.markDirty(vmUuid); + + if (markSuccess) { + // markDirty 成功 → 安全清除 stale 标记 + 递增 staleRecoveryCount + SQL.New("UPDATE VmMetadataPathFingerprintVO " + + "SET lastFlushFailed = 0, staleRecoveryCount = staleRecoveryCount + 1 " + + "WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmUuid) + .execute(); + requeued++; + } else { + // markDirty 失败 → 保留 lastFlushFailed=true,下轮重试 + logger.warn("[MetadataStaleRecovery] markDirty failed for vm={}, " + + "keeping lastFlushFailed=true for next retry cycle", vmUuid); + } + } + + logger.info("[MetadataStaleRecovery] processed {} stale VMs: requeued={}, circuitBroken={}", + staleVms.size(), requeued, circuitBroken); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataBuilder.java b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataBuilder.java new file mode 100644 index 00000000000..85d7527a445 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataBuilder.java @@ -0,0 +1,345 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.VolumeSnapshotTree; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.storage.snapshot.reference.VolumeSnapshotReferenceTreeVO; +import org.zstack.header.storage.snapshot.reference.VolumeSnapshotReferenceTreeVO_; +import org.zstack.header.storage.snapshot.reference.VolumeSnapshotReferenceVO; +import org.zstack.header.storage.snapshot.reference.VolumeSnapshotReferenceVO_; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.tag.SystemTagVO_; +import org.zstack.header.vm.*; +import org.zstack.header.volume.VolumeType; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.resourceconfig.ResourceConfigVO; +import org.zstack.resourceconfig.ResourceConfigVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 构建虚拟机元数据 payload 的 Spring Component。 + * + *

从 VmInstanceBase 中提取出来,以获得 Spring AOP 代理的 {@code @Transactional} 支持。 + * VmInstanceBase 实例不是 Spring 单例 Bean,其内部方法调用不经过 AOP 代理, + * 因此 {@code @Transactional} 注解在 VmInstanceBase 自身方法上不生效。

+ * + *

{@link #buildVmInstanceMetadata(String)} 执行 6+ 条 SELECT 查询, + * 必须在同一个 REPEATABLE READ 事务快照内完成,以保证读一致性。

+ * + * @see VmInstanceMetadataDTO + */ +public class VmMetadataBuilder { + private static final CLogger logger = Utils.getLogger(VmMetadataBuilder.class); + + /** Payload 大小预警阈值(8 MB) */ + public static final int WARN_THRESHOLD = 8 * 1024 * 1024; + + /** Payload 大小拒绝阈值(30 MB) */ + public static final int REJECT_THRESHOLD = 30 * 1024 * 1024; + + @Autowired + private DatabaseFacade dbf; + + /** + * 从 DB 全量构建指定 VM 的元数据 JSON 字符串。 + * + *

使用 {@code @Transactional(readOnly = true)} 确保所有 SELECT 查询 + * 在同一个 InnoDB REPEATABLE READ 事务快照内执行,保证读一致性。

+ * + * @param vmInstanceUuid 目标虚拟机 UUID + * @return 元数据 JSON 字符串;若 VM 不符合构建条件则返回 null + */ + @Transactional(readOnly = true) + public String buildVmInstanceMetadata(String vmInstanceUuid) { + // ── 查询 VM 本体 ── + VmInstanceVO vm = Q.New(VmInstanceVO.class).eq(VmInstanceVO_.uuid, vmInstanceUuid).find(); + if (vm == null) { + logger.warn(String.format("VM[uuid:%s] not found, skip metadata build", vmInstanceUuid)); + return null; + } + + // ── UserVm 类型检查 ── + if (!VmInstanceConstant.USER_VM_TYPE.equals(vm.getType())) { + logger.debug(String.format("VM[uuid:%s] type is [%s], not UserVm, skip metadata build", + vmInstanceUuid, vm.getType())); + return null; + } + + // ── 云盘(挂载的 + 已卸载但 lastVmInstanceUuid 指向本 VM 的) ── + List allVolumes = new ArrayList<>(); + allVolumes.addAll(Q.New(VolumeVO.class).eq(VolumeVO_.vmInstanceUuid, vmInstanceUuid).list()); + allVolumes.addAll(Q.New(VolumeVO.class).isNull(VolumeVO_.vmInstanceUuid) + .eq(VolumeVO_.lastVmInstanceUuid, vmInstanceUuid).list()); + + // ── 共享盘排除 ── + List volumes = allVolumes.stream() + .filter(v -> !v.isShareable()) + .collect(Collectors.toList()); + + // ── Root Volume 检查 ── + boolean hasRootVolume = volumes.stream() + .anyMatch(v -> VolumeType.Root.toString().equals(v.getType())); + if (!hasRootVolume) { + logger.warn(String.format("VM[uuid:%s] has no root volume, skip metadata build", vmInstanceUuid)); + return null; + } + + // ── 确定性排序:volumes by uuid ── + volumes.sort(Comparator.comparing(VolumeVO::getUuid)); + + VmInstanceMetadataDTO dto = new VmInstanceMetadataDTO(); + + // ── schemaVersion ── + dto.schemaVersion = dbf.getDbVersion(); + + // ── vmCategory(先判缓存再判模板) ── + if (Q.New(TemplatedVmInstanceCacheVO.class) + .eq(TemplatedVmInstanceCacheVO_.cacheVmInstanceUuid, vmInstanceUuid) + .isExists()) { + dto.vmCategory = VmMetadataCategory.TEMPLATE_CACHE; + } else if (Q.New(TemplatedVmInstanceVO.class) + .eq(TemplatedVmInstanceVO_.uuid, vmInstanceUuid) + .isExists()) { + dto.vmCategory = VmMetadataCategory.TEMPLATE; + } else { + dto.vmCategory = VmMetadataCategory.REGULAR; + } + + // ── VM 本体 ── + dto.vm = buildResourceMetadata(vm.getUuid(), vm); + + // ── 云盘(VolumeResourceMetadata,含引用数据) ── + List volumeUuids = volumes.stream().map(VolumeVO::getUuid).collect(Collectors.toList()); + dto.volumes = new ArrayList<>(); + for (VolumeVO vol : volumes) { + dto.volumes.add(buildVolumeResourceMetadata(vol)); + } + + // ── 网卡(排序 by uuid) ── + List nics = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vmInstanceUuid).list(); + nics.sort(Comparator.comparing(VmNicVO::getUuid)); + dto.nics = new ArrayList<>(); + nics.forEach(n -> dto.nics.add(buildResourceMetadata(n.getUuid(), n))); + + // ── 快照(BFS 拓扑排序,扁平列表) ── + if (!volumeUuids.isEmpty()) { + List allSnapshots = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.volumeUuid, volumeUuids).list(); + + if (allSnapshots.isEmpty()) { + dto.snapshots = Collections.emptyList(); + } else { + List sorted = topoSortSnapshots(allSnapshots, vmInstanceUuid); + dto.snapshots = sorted.stream() + .map(JSONObjectUtil::toJsonString) + .collect(Collectors.toList()); + } + } else { + dto.snapshots = Collections.emptyList(); + } + + // ── 快照组(排序 by uuid) ── + List groups = Q.New(VolumeSnapshotGroupVO.class) + .eq(VolumeSnapshotGroupVO_.vmInstanceUuid, vmInstanceUuid).list(); + groups.sort(Comparator.comparing(VolumeSnapshotGroupVO::getUuid)); + dto.snapshotGroups = groups.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + + // ── 快照组关联引用(复合键排序:volumeSnapshotGroupUuid + volumeUuid) ── + List groupUuids = groups.stream() + .map(VolumeSnapshotGroupVO::getUuid).collect(Collectors.toList()); + if (!groupUuids.isEmpty()) { + List refs = Q.New(VolumeSnapshotGroupRefVO.class) + .in(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuids).list(); + refs.sort(Comparator.comparing(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid) + .thenComparing(VolumeSnapshotGroupRefVO::getVolumeUuid)); + dto.snapshotGroupRefs = refs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + } else { + dto.snapshotGroupRefs = Collections.emptyList(); + } + + // ── 序列化 & payload 大小检查 ── + String json = JSONObjectUtil.toJsonString(dto); + int payloadSize = json.getBytes(StandardCharsets.UTF_8).length; + if (payloadSize > REJECT_THRESHOLD) { + logger.error(String.format("VM[uuid:%s] metadata payload size %d bytes exceeds reject threshold %d bytes, " + + "skip metadata build", vmInstanceUuid, payloadSize, REJECT_THRESHOLD)); + return null; + } + if (payloadSize > WARN_THRESHOLD) { + logger.warn(String.format("VM[uuid:%s] metadata payload size %d bytes exceeds warn threshold %d bytes", + vmInstanceUuid, payloadSize, WARN_THRESHOLD)); + } + + return json; + } + + /** + * 对所有快照进行 BFS 拓扑排序。 + * + *

按 volumeUuid 分组,再按 treeUuid 分组(双层 TreeMap 保证 ASC 排序), + * 同一 tree 内使用 {@link VolumeSnapshotTree#fromVOs(List)} + + * {@link VolumeSnapshotTree#levelOrderTraversal()} 进行 BFS 层序遍历。

+ * + * @param allSnapshots 待排序的全部快照 VO + * @param vmUuid VM UUID(仅用于日志) + * @return 拓扑排序后的快照 VO 列表 + */ + private List topoSortSnapshots(List allSnapshots, String vmUuid) { + // 双层 TreeMap 分组:volumeUuid → treeUuid → List + Map>> byVolumeThenTree = + allSnapshots.stream().collect(Collectors.groupingBy( + VolumeSnapshotVO::getVolumeUuid, TreeMap::new, + Collectors.groupingBy(VolumeSnapshotVO::getTreeUuid, + TreeMap::new, Collectors.toList()))); + + List result = new ArrayList<>(); + // 按 volumeUuid ASC → treeUuid ASC 遍历 + for (Map> treesInVolume : byVolumeThenTree.values()) { + for (List treeSnapshots : treesInVolume.values()) { + VolumeSnapshotTree tree = VolumeSnapshotTree.fromVOs(treeSnapshots); + List ordered = tree.levelOrderTraversal(); + for (VolumeSnapshotInventory inv : ordered) { + VolumeSnapshotVO found = findSnapshotByUuid(treeSnapshots, inv.getUuid()); + if (found != null) { + result.add(found); + } + } + } + } + + // 循环引用防护:若 BFS 遗漏了快照,追加到结尾 + if (result.size() < allSnapshots.size()) { + Set resultUuids = result.stream() + .map(VolumeSnapshotVO::getUuid).collect(Collectors.toSet()); + List missing = allSnapshots.stream() + .filter(s -> !resultUuids.contains(s.getUuid())) + .sorted(Comparator.comparing(VolumeSnapshotVO::getUuid)) + .collect(Collectors.toList()); + logger.warn(String.format("Unreachable snapshots detected for VM[uuid:%s]: %d out of %d, " + + "possible circular reference. Appending missing snapshots by uuid ASC.", + vmUuid, missing.size(), allSnapshots.size())); + result.addAll(missing); + } + + return result; + } + + /** + * 从快照列表中按 UUID 查找。 + */ + private VolumeSnapshotVO findSnapshotByUuid(List snapshots, String uuid) { + for (VolumeSnapshotVO s : snapshots) { + if (s.getUuid().equals(uuid)) { + return s; + } + } + return null; + } + + /** + * 构建单个 Volume 的 {@link VolumeResourceMetadata}。 + * + *

包含 VO JSON、SystemTag、ResourceConfig 以及 + * 该 Volume 关联的快照引用(VolumeSnapshotReferenceVO)和引用树(VolumeSnapshotReferenceTreeVO)。

+ * + * @param vol VolumeVO 对象 + * @return 填充完毕的 VolumeResourceMetadata + */ + private VolumeResourceMetadata buildVolumeResourceMetadata(VolumeVO vol) { + VolumeResourceMetadata meta = new VolumeResourceMetadata(); + meta.resourceUuid = vol.getUuid(); + meta.vo = JSONObjectUtil.toJsonString(vol); + + // SystemTag: 排序 by uuid → JSON 数组 → Base64 + List tagVOs = Q.New(SystemTagVO.class) + .eq(SystemTagVO_.resourceUuid, vol.getUuid()).list(); + tagVOs.sort(Comparator.comparing(SystemTagVO::getUuid)); + // TODO: 白名单过滤(CoreMemorySnapshotConfigs 当前不存在,待后续实现) + List tagJsons = tagVOs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + meta.systemTags = Base64.getEncoder().encodeToString( + JSONObjectUtil.toJsonString(tagJsons).getBytes(StandardCharsets.UTF_8)); + + // ResourceConfig: 排序 by uuid → JSON 数组 → Base64 + List cfgVOs = Q.New(ResourceConfigVO.class) + .eq(ResourceConfigVO_.resourceUuid, vol.getUuid()).list(); + cfgVOs.sort(Comparator.comparing(ResourceConfigVO::getUuid)); + // TODO: 白名单过滤(CoreMemorySnapshotConfigs 当前不存在,待后续实现) + List cfgJsons = cfgVOs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + meta.resourceConfigs = Base64.getEncoder().encodeToString( + JSONObjectUtil.toJsonString(cfgJsons).getBytes(StandardCharsets.UTF_8)); + + // 快照引用:按 id 排序 + List refs = Q.New(VolumeSnapshotReferenceVO.class) + .eq(VolumeSnapshotReferenceVO_.referenceVolumeUuid, vol.getUuid()).list(); + refs.sort(Comparator.comparing(VolumeSnapshotReferenceVO::getId)); + meta.snapshotReferences = refs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + + // 快照引用树:按 uuid 排序 + List trees = Q.New(VolumeSnapshotReferenceTreeVO.class) + .eq(VolumeSnapshotReferenceTreeVO_.rootVolumeUuid, vol.getUuid()).list(); + trees.sort(Comparator.comparing(VolumeSnapshotReferenceTreeVO::getUuid)); + meta.snapshotReferenceTrees = trees.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + + return meta; + } + + /** + * 构建单个资源的 {@link VmInstanceMetadataDTO.ResourceMetadata}。 + * + *

VO 全量 JSON 明文存储;SystemTagVO 和 ResourceConfigVO 整体列表序列化为 JSON 数组后 + * 一次性 Base64 编码,以保护可能包含的密码、密钥等敏感信息。

+ * + * @param resourceUuid 资源 UUID + * @param vo 资源 VO 对象(VmInstanceVO / VmNicVO) + * @return 填充完毕的 ResourceMetadata + */ + private VmInstanceMetadataDTO.ResourceMetadata buildResourceMetadata(String resourceUuid, Object vo) { + VmInstanceMetadataDTO.ResourceMetadata meta = new VmInstanceMetadataDTO.ResourceMetadata(); + meta.resourceUuid = resourceUuid; + meta.vo = JSONObjectUtil.toJsonString(vo); + + // SystemTagVO: 排序 by uuid → JSON 数组 → Base64 + List tagVOs = Q.New(SystemTagVO.class) + .eq(SystemTagVO_.resourceUuid, resourceUuid).list(); + tagVOs.sort(Comparator.comparing(SystemTagVO::getUuid)); + // TODO: 白名单过滤(CoreMemorySnapshotConfigs 当前不存在,待后续实现) + List tagJsons = tagVOs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + meta.systemTags = Base64.getEncoder().encodeToString( + JSONObjectUtil.toJsonString(tagJsons).getBytes(StandardCharsets.UTF_8)); + + // ResourceConfigVO: 排序 by uuid → JSON 数组 → Base64 + List cfgVOs = Q.New(ResourceConfigVO.class) + .eq(ResourceConfigVO_.resourceUuid, resourceUuid).list(); + cfgVOs.sort(Comparator.comparing(ResourceConfigVO::getUuid)); + // TODO: 白名单过滤(CoreMemorySnapshotConfigs 当前不存在,待后续实现) + List cfgJsons = cfgVOs.stream() + .map(JSONObjectUtil::toJsonString).collect(Collectors.toList()); + meta.resourceConfigs = Base64.getEncoder().encodeToString( + JSONObjectUtil.toJsonString(cfgJsons).getBytes(StandardCharsets.UTF_8)); + + return meta; + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataDirtyMarker.java b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataDirtyMarker.java new file mode 100644 index 00000000000..b08b8c5cc65 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/metadata/VmMetadataDirtyMarker.java @@ -0,0 +1,1201 @@ +package org.zstack.compute.vm.metadata; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +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.db.SQLBatch; +import org.zstack.core.db.SimpleQuery; +import org.zstack.core.thread.ChainTask; +import org.zstack.core.thread.PeriodicTask; +import org.zstack.core.thread.SyncTaskChain; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.header.Component; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.managementnode.ManagementNodeChangeListener; +import org.zstack.header.managementnode.ManagementNodeInventory; +import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint; +import org.zstack.header.message.MessageReply; +import org.zstack.header.vm.UpdateVmInstanceMetadataMsg; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceState; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.VmMetadataDirtyVO; +import org.zstack.header.vm.VmMetadataDirtyVO_; +import org.zstack.header.vm.VmMetadataPathFingerprintVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +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.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * VM 元数据 Dirty Mark + Poller 机制的核心实现。 + * + *

职责

+ *
    + *
  • {@link #markDirty(String)} — 标脏入口,INSERT ON DUPLICATE KEY UPDATE + 立即唤醒
  • + *
  • {@link MetadataDirtyPoller} — 周期轮询安全网,处理退避到期行、MN 宕机释放行等
  • + *
  • {@link #claimAndFlush()} — CAS 认领 + 提交刷写
  • + *
  • {@link #doFlush} — 构建 payload → 发送 UpdateVmInstanceMetadataMsg → 成功/失败处理
  • + *
  • {@link ManagementNodeChangeListener#nodeLeft} — MN 宕机后立即接管
  • + *
+ * + *

串行化保证(四层)

+ *
+ *   Layer 1 — DB CAS 认领:UPDATE WHERE managementNodeUuid IS NULL → 同一行只被一个 MN 处理
+ *   Layer 2 — AtomicInteger 全局限流:globalFlushInFlight(默认上限 10)
+ *   Layer 3 — ChainTask 队列 "update-vm-{vmUuid}-metadata":syncLevel=1, maxPending=1
+ *   Layer 4 — 主存储级队列 "update-metadata-on-ps-{psUuid}"(在 PS handler 内部实现)
+ * 
+ * + * @see VmMetadataDirtyVO + * @see VmMetadataUpdateInterceptor + */ +public class VmMetadataDirtyMarker implements Component, ManagementNodeChangeListener, ManagementNodeReadyExtensionPoint { + private static final CLogger logger = Utils.getLogger(VmMetadataDirtyMarker.class); + + // ===================================================================== + // 常量 + // ===================================================================== + + // 指数退避参数改为 GlobalConfig(C-RB-04),详见 onFlushFailure()。 + + // ===================================================================== + // 注入 + // ===================================================================== + + @Autowired + private CloudBus bus; + + @Autowired + private DatabaseFacade dbf; + + @Autowired + private ThreadFacade thdf; + + // ===================================================================== + // Poller 状态 + // ===================================================================== + + private Future pollerFuture; + private Future zombieCleanupFuture; + + // ===================================================================== + // 全局并发限流(Δ-1:替代原嵌套 ChainTask 外层) + // ===================================================================== + + /** 当前正在 flight 的 flush 任务数。per-MN JVM 本地计数器。 */ + private final AtomicInteger globalFlushInFlight = new AtomicInteger(0); + + // ===================================================================== + // Component 生命周期 + // ===================================================================== + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + stopPoller(); + stopZombieCleanupTask(); + return true; + } + + // ===================================================================== + // ManagementNodeReadyExtensionPoint:MN 就绪后启动 Poller + // ===================================================================== + + @Override + public void managementNodeReady() { + recoverStalledMigrationPauses(); // C-01C-8: must run before Poller starts + startPoller(); + startZombieCleanupTask(); + + VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.installUpdateExtension((oldValue, newValue) -> { + restartPoller(); + }); + + // §9a: 监听 vm.metadata.enabled 开关切换 + VmGlobalConfig.VM_METADATA.installUpdateExtension((oldValue, newValue) -> { + boolean wasEnabled = Boolean.parseBoolean(oldValue.toString()); + boolean nowEnabled = Boolean.parseBoolean(newValue.toString()); + if (!wasEnabled && nowEnabled) { + // false → true:分批全量初始化(§9a.1) + logger.info("[MetadataDirty] vm.metadata.enabled toggled from false to true, starting batch initialization"); + submitBatchInitialization(); + } else if (wasEnabled && !nowEnabled) { + // true → false:清理 PathFingerprint(§9a.2 讨论 Δ-10) + logger.info("[MetadataDirty] vm.metadata.enabled toggled from true to false, cleaning up PathFingerprints"); + cleanupPathFingerprints(); + } + }); + + // §9.1: 升级后全量刷新 — 检查 DB 版本与 lastRefreshVersion 是否一致 + scheduleUpgradeRefreshIfNeeded(); + } + + /** + * 恢复因存储迁移中断而"永久暂停"的脏标记行。 + * + *

存储迁移期间 Poller 会将相关 dirty 行的 nextRetryTime 设为 2099-12-31 23:59:59 + * 以防止 flush 竞争。如果迁移流程崩溃(MN 宕机),这些行会卡在该时间点永远不被处理。

+ * + *

本方法在 MN 重启后、Poller 启动前执行,将所有"远未来"暂停行重置为可处理状态。

+ * + * @see Part 01c §1.6 迁移暂停恢复 + */ + private void recoverStalledMigrationPauses() { + int recovered = SQL.New( + "UPDATE VmMetadataDirtyVO " + + "SET nextRetryTime = NULL, retryCount = 0 " + + "WHERE nextRetryTime = '2099-12-31 23:59:59'") + .execute(); + if (recovered > 0) { + logger.warn(String.format("[MetadataDirty] Recovered %d dirty rows with stalled migration pause (nextRetryTime far in future)", recovered)); + } + } + + // ===================================================================== + // ManagementNodeChangeListener:MN 拓扑变化处理 + // ===================================================================== + + /** Timestamp of the most recent nodeLeft event, used by §9.1 M3 recent-nodeLeft check. */ + private volatile long lastNodeLeftTimestamp = 0; + + @Override + public void nodeLeft(ManagementNodeInventory inv) { + // MN 宕机 → FK SET_NULL 已释放其认领的 dirty 行 + // C-02B-1 §7.2: 延迟 N 秒后再触发 claimAndFlush(),降低 zombie MN 并发写入概率 + long delaySec = VmGlobalConfig.VM_METADATA_NODE_LEFT_DELAY.value(Long.class); + logger.info(String.format("[MetadataDirty] node[%s] left, scheduling claim and flush after %ds delay", + inv.getUuid(), delaySec)); + + // M3 修复:记录 nodeLeft 时间戳,供 §9.1 升级刷新 recent-nodeLeft 检查使用 + lastNodeLeftTimestamp = System.currentTimeMillis(); + + thdf.submit(new org.zstack.core.thread.Task() { + @Override + public String getName() { + return "metadata-dirty-node-left-claim"; + } + + @Override + public Void call() { + try { + TimeUnit.SECONDS.sleep(delaySec); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("[MetadataDirty] nodeLeft delay interrupted"); + return null; + } + claimAndFlush(); + return null; + } + }); + } + + @Override + public void nodeJoin(ManagementNodeInventory inv) { + // 无需特殊处理,新 MN 的 Poller 正常启动即可 + } + + @Override + public void iAmDead(ManagementNodeInventory inv) { + // 本 MN 即将死亡,不做处理 + // FK SET_NULL 会自动释放本 MN 认领的行 + } + + @Override + public void iJoin(ManagementNodeInventory inv) { + // 由 managementNodeReady 启动 Poller + } + + // ===================================================================== + // markDirty — 标脏入口(公开方法) + // ===================================================================== + + /** + * 将指定 VM 标记为"元数据脏",需要重新写入主存储。 + * + *

使用 INSERT IGNORE + UPDATE 两步(C-DM-01: Galera 集群兼容),保证:

+ *
    + *
  • 行不存在 → INSERT IGNORE 新建(dirtyVersion=1)
  • + *
  • 行已存在 → UPDATE dirtyVersion +1(标记"有新变更")
  • + *
  • 竞态 inserted==0 && updated==0 → 重新 INSERT IGNORE(Q19 修复)
  • + *
  • storageStructureChange 使用 OR 升级策略
  • + *
  • 不重置 retryCount / managementNodeUuid / nextRetryTime
  • + *
+ * + *

markDirty 后立即调用 {@link #triggerFlushForVm(String)}, + * 尝试认领并提交刷写,消除最长 N 秒的 Poller 等待延迟。

+ * + * @param vmInstanceUuid 目标虚拟机 UUID + * @param storageStructureChange 是否涉及存储结构变更 + * @return true 如果标脏成功(供 MetadataStaleRecoveryTask DP-03 使用) + */ + public boolean markDirty(String vmInstanceUuid, boolean storageStructureChange) { + // 前置检查:功能开关 + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return false; + } + + // 前置检查:仅处理 KVM 虚拟化 + UserVm 类型的 VM + // 非 KVM(如 Simulator)或非 UserVm(如 ApplianceVm)不产生元数据 + boolean isTargetVm = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmInstanceUuid) + .eq(VmInstanceVO_.type, VmInstanceConstant.USER_VM_TYPE) + .eq(VmInstanceVO_.hypervisorType, VmInstanceConstant.KVM_HYPERVISOR_TYPE) + .isExists(); + if (!isTargetVm) { + logger.trace(String.format("[MetadataDirty] vm[uuid:%s] is not KVM UserVm, skipping markDirty", + vmInstanceUuid)); + return false; + } + + try { + // C-DM-01: Galera 集群兼容写法,避免 INSERT ON DUPLICATE KEY 在高并发下死锁 + // Step 1: INSERT IGNORE(新行) + int inserted = SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO " + + "(vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "VALUES (:vmUuid, 1, :ssc)") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + + // Step 2: 仅在行已存在时执行 UPDATE(dirtyVersion +1, storageStructureChange OR 升级) + if (inserted == 0) { + int updated = SQL.New("UPDATE VmMetadataDirtyVO " + + "SET dirtyVersion = dirtyVersion + 1, " + + " storageStructureChange = storageStructureChange OR :ssc " + + "WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + + // Q19 修复:INSERT IGNORE 返回 0(行已存在)但 UPDATE 也返回 0(行被并发删除) + // 竞态窗口:INSERT IGNORE → onFlushSuccess DELETE → UPDATE(行已不存在) + // 此时必须重新 INSERT,否则本次 markDirty 对应的 DB 变更将丢失 + if (updated == 0) { + SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO " + + "(vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "VALUES (:vmUuid, 1, :ssc)") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + } + } + + logger.debug(String.format("[MetadataDirty] marked dirty for vm[uuid:%s], storageStructureChange=%s", + vmInstanceUuid, storageStructureChange)); + + // 立即唤醒:尝试认领并提交刷写,不等待 Poller 轮询 + triggerFlushForVm(vmInstanceUuid); + return true; + } catch (Exception e) { + logger.warn(String.format("[MetadataDirty] markDirty failed for vm[uuid:%s]: %s", + vmInstanceUuid, e.getMessage())); + return false; + } + } + + /** + * 标脏入口(便捷重载,默认 storageStructureChange=false,即 CONFIG 级别)。 + * + * @param vmInstanceUuid 目标虚拟机 UUID + * @return true 如果标脏成功 + */ + public boolean markDirty(String vmInstanceUuid) { + return markDirty(vmInstanceUuid, false); + } + + // ===================================================================== + // triggerFlushForVm — 立即唤醒(单 VM) + // ===================================================================== + + /** + * 立即尝试认领并刷写指定 VM 的 dirty 行。 + * 若行已被认领或处于退避期,跳过(Poller 安全网会处理)。 + * + *

Q20 修复:findStaleClaimOwner 可能返回 null(无 stale claim)。 + * SQL 的 OR 分支使用 :staleId 参数,当 staleId=null 时 + * MySQL 会将 {@code managementNodeUuid = NULL} 解析为 FALSE(SQL 三值逻辑), + * 不会误匹配任何行。但为避免依赖此隐式行为,显式处理: + * staleId=null 时仅使用 IS NULL 分支,不包含 stale 接管条件。

+ */ + private void triggerFlushForVm(String vmUuid) { + String myId = Platform.getManagementServerId(); + long staleMinutes = VmGlobalConfig.VM_METADATA_TRIGGER_FLUSH_STALE.value(Long.class); + String staleId = findStaleClaimOwner(vmUuid, Duration.ofMinutes(staleMinutes)); + + String sql; + if (staleId != null) { + sql = "UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE vmInstanceUuid = :vmUuid " + + "AND (managementNodeUuid IS NULL " + + " OR (managementNodeUuid = :staleId AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL " + staleMinutes + " MINUTE)) " + + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; + } else { + sql = "UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE vmInstanceUuid = :vmUuid " + + "AND managementNodeUuid IS NULL " + + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; + } + + int claimed = SQL.New(sql) + .param("myId", myId) + .param("staleId", staleId) + .param("vmUuid", vmUuid) + .execute(); + + if (claimed == 0) { + logger.debug(String.format("[MetadataDirty] triggerFlushForVm skip claim, vmUuid=%s, " + + "reason=already-claimed-or-backoff", vmUuid)); + return; + } + + VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + // DP-07 说明:dirty == null 是合法场景。CAS UPDATE 成功后、findByUuid 前, + // 若同 MN 上一个 running flush 的 onFlushSuccess() 恰好执行了条件 DELETE, + // 则该行已被删除。此时直接 return 即可——数据已经是最新的。 + if (dirty == null) { + return; + } + + submitFlushTask(dirty); + } + + // ===================================================================== + // Poller — 轮询安全网 + // ===================================================================== + + /** + * 内部 PeriodicTask 实现。 + * + *

Poller 角色定位:markDirty 后的 triggerFlushForVm 已覆盖常规场景。 + * Poller 降级为安全网,负责处理: + *

    + *
  • 退避中的行(nextRetryTime 到期后才能认领)
  • + *
  • MN 宕机后 FK SET_NULL 释放的孤儿行
  • + *
  • triggerFlushForVm 认领失败的行(已被其他 MN Poller 认领)
  • + *
+ */ + private class MetadataDirtyPoller implements PeriodicTask { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.value(Long.class); + } + + @Override + public String getName() { + return "vm-metadata-dirty-poller"; + } + + @Override + public void run() { + claimAndFlush(); + } + } + + private synchronized void startPoller() { + if (pollerFuture != null) { + pollerFuture.cancel(false); + } + pollerFuture = thdf.submitPeriodicTask(new MetadataDirtyPoller()); + logger.info("[MetadataDirty] poller started"); + } + + private synchronized void stopPoller() { + if (pollerFuture != null) { + pollerFuture.cancel(false); + pollerFuture = null; + logger.info("[MetadataDirty] poller stopped"); + } + } + + private void restartPoller() { + logger.info("[MetadataDirty] restarting poller due to config change"); + startPoller(); + } + + // ===================================================================== + // claimAndFlush — 认领 + 提交刷写(Poller 和 nodeLeft 共用) + // ===================================================================== + + /** + * CAS 认领一批 dirty 行并提交刷写。 + */ + private void claimAndFlush() { + // 功能关闭时跳过,避免 Poller 空转(P2-2.2 修复) + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + List claimed = claimDirtyRows(); + for (VmMetadataDirtyVO dirty : claimed) { + submitFlushTask(dirty); + } + } + + /** + * CAS 原子认领一批 dirty 行。 + * + *

单条 UPDATE 天然原子,无锁等待,无死锁风险。

+ *

DP-05 修复:僵尸 claim 清理已提取为独立低频任务 {@link #cleanupZombieClaims()}。

+ * + * @return 认领到的 dirty 行列表 + */ + private List claimDirtyRows() { + // Step 1: CAS 原子认领 + // Q17 修复:ORDER BY lastOpDate ASC + vmInstanceUuid ASC(稳定 tiebreaker) + // C-CL-02: lastClaimTime = CURRENT_TIMESTAMP + int claimed = SQL.New("UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE managementNodeUuid IS NULL " + + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP) " + + "ORDER BY lastOpDate ASC, vmInstanceUuid ASC " + + "LIMIT :batchSize") + .param("myId", Platform.getManagementServerId()) + .param("batchSize", VmGlobalConfig.VM_METADATA_DIRTY_BATCH_SIZE.value(Integer.class)) + .execute(); + + if (claimed == 0) { + return Collections.emptyList(); + } + + // Step 2: 查询刚认领到的行(DP-01 修复:增加 lastClaimTime 过滤, + // 仅返回本轮 CAS 认领的行,避免与 triggerFlushForVm 并发认领的行混入) + Timestamp thisCycleCutoff = Timestamp.from(Instant.now().minus(Duration.ofSeconds(5))); + return Q.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.managementNodeUuid, Platform.getManagementServerId()) + .gte(VmMetadataDirtyVO_.lastClaimTime, thisCycleCutoff) + .list(); + } + + // ===================================================================== + // submitFlushTask — AtomicInteger 全局限流 + per-VM 串行去重(Δ-1 重构) + // ===================================================================== + + /** + * 将 dirty 行的刷写任务提交到 ChainTask 队列。 + * + *

Δ-1 重构:原嵌套 ChainTask(外层全局限流 + 内层 per-VM 串行) + * 改为 AtomicInteger 全局限流 + 单层 per-VM ChainTask。原因:

+ *
    + *
  1. 嵌套 ChainTask 的 outerChain.next() 在 exceedMaxPendingCallback 中直接调用 + * 导致 outer slot 提前释放,全局限流语义被破坏
  2. + *
  3. 嵌套结构难以推断 Chain 生命周期
  4. + *
  5. AtomicInteger 语义简单明确:flush 开始 increment、完成 decrement、超限 skip
  6. + *
+ */ + private void submitFlushTask(VmMetadataDirtyVO dirty) { + final String vmUuid = dirty.getVmInstanceUuid(); + + // 全局并发检查 + int maxConcurrent = VmGlobalConfig.VM_METADATA_GLOBAL_MAX_CONCURRENT.value(Integer.class); + if (globalFlushInFlight.get() >= maxConcurrent) { + // 全局并发已满,释放 claim,Poller 下轮重试 + releaseClaim(vmUuid); + return; + } + globalFlushInFlight.incrementAndGet(); + + // 单层 per-VM 串行 + 去重 + thdf.chainSubmit(new ChainTask(null) { + @Override + public String getSyncSignature() { + return String.format("update-vm-%s-metadata", vmUuid); + } + + @Override + public int getSyncLevel() { + return 1; + } + + @Override + protected int getMaxPendingTasks() { + return 1; + } + + @Override + protected String getDeduplicateString() { + return getSyncSignature(); + } + + @Override + protected void exceedMaxPendingCallback() { + // Δ-1 改进:在单层结构中,exceed 时直接 decrement 并释放 claim + globalFlushInFlight.decrementAndGet(); + releaseClaim(vmUuid); + } + + @Override + public void run(final SyncTaskChain chain) { + doFlush(dirty, () -> { + globalFlushInFlight.decrementAndGet(); + chain.next(); + }); + } + + @Override + public String getName() { + return String.format("update-vm-%s-metadata-task", vmUuid); + } + }); + } + + // ===================================================================== + // doFlush — 核心刷写逻辑 + // ===================================================================== + + /** + * 执行元数据刷写。 + * + *

流程:

+ *
    + *
  1. P2 修复:重新从 DB 读取 dirty 行(获取最新 storageStructureChange/dirtyVersion)
  2. + *
  3. 前置检查(VM 是否存在、C-FL-08 Destroyed 状态过滤)
  4. + *
  5. 发送 UpdateVmInstanceMetadataMsg(由 VmInstanceBase 构建 payload 并写入主存储)
  6. + *
  7. 成功 → onFlushSuccess(条件删除 dirty 行)
  8. + *
  9. 失败 → onFlushFailure(指数退避或放弃)
  10. + *
+ */ + private void doFlush(VmMetadataDirtyVO dirty, Runnable chainNext) { + String vmUuid = dirty.getVmInstanceUuid(); + + // P2 修复:重新从 DB 读取 dirty 行,获取最新的 storageStructureChange 和 dirtyVersion。 + // 原因:submitFlushTask 传入的 dirty 对象是 CAS 认领时的缓存快照,排队等待期间 + // 可能有新的 markDirty(storageStructureChange=true) 通过 OR 升级了该字段。 + VmMetadataDirtyVO latestDirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + if (latestDirty == null) { + // VM 已删除(FK CASCADE)或 onFlushSuccess 已删除该行 + chainNext.run(); + return; + } + + // 0. 记录刷写开始时的 dirtyVersion 快照(使用最新值) + long snapshotVersion = latestDirty.getDirtyVersion(); + + // C-02B-2 §7.6: Fence Check — 防止 zombie MN(GC pause 恢复后)并发写入 + // 验证 dirty 行仍被本 MN 认领。若认领已被 nodeLeft → FK SET_NULL → 其他 MN 接管, + // 则本 MN 的旧 flush 任务必须立即中止。 + if (!Platform.getManagementServerId().equals(latestDirty.getManagementNodeUuid())) { + logger.warn(String.format("[MetadataDirty] Lost claim on vm[uuid:%s], " + + "expected mnUuid=%s but got %s, abort flush write", + vmUuid, Platform.getManagementServerId(), latestDirty.getManagementNodeUuid())); + chainNext.run(); + return; + } + + // 1. 前置检查:VM 是否存在 + if (!dbf.isExist(vmUuid, VmInstanceVO.class)) { + // VM 已删除,FK CASCADE 应已删除 dirty 行,兜底删除 + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .delete(); + chainNext.run(); + return; + } + + // 1b. C-FL-08:过滤 Destroyed 状态的 VM + // VM 正在销毁过程中(state=Destroyed),EO 尚未物理删除,FK CASCADE 未触发。 + // 此时刷写元数据无意义——销毁完成后 EO 删除时 dirty 行会被级联清理。 + VmInstanceState vmState = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmUuid) + .select(VmInstanceVO_.state) + .findValue(); + if (vmState == VmInstanceState.Destroyed) { + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .delete(); + chainNext.run(); + return; + } + + // 2. 发送到 VmInstanceBase 处理(由 VmInstanceBase 内部构建 payload 并写入主存储) + // C-TM-03:超时 ≥ 5 分钟 + UpdateVmInstanceMetadataMsg msg = new UpdateVmInstanceMetadataMsg(); + msg.setUuid(vmUuid); + msg.setStorageStructureChange(latestDirty.isStorageStructureChange()); + msg.setTimeout(TimeUnit.MINUTES.toMillis(5)); + bus.makeLocalServiceId(msg, VmInstanceConstant.SERVICE_ID); + + bus.send(msg, new CloudBusCallBack(null) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + onFlushSuccess(vmUuid, snapshotVersion); + } else { + onFlushFailure(vmUuid, reply.getError()); + } + chainNext.run(); + } + }); + } + + // ===================================================================== + // onFlushSuccess — 刷写成功处理(dirtyVersion 条件删除) + // ===================================================================== + + /** + * 刷写成功后的处理。 + * + *

Δ-2 修复:使用 SQLBatch 替代 @Transactional,避免 self-invocation 陷阱。

+ * + *

条件删除:仅当 dirtyVersion == snapshotVersion 时删除, + * 即"刷写期间没有新的 markDirty 到来"。

+ * + *

如果 dirtyVersion > snapshotVersion,说明刷写期间有新变更, + * 释放认领让 triggerFlush / Poller 重新处理。

+ */ + private void onFlushSuccess(String vmUuid, long snapshotVersion) { + new SQLBatch() { + @Override + protected void scripts() { + // 条件删除:仅当 dirtyVersion == snapshotVersion 时删除 + int deleted = SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .eq(VmMetadataDirtyVO_.dirtyVersion, snapshotVersion) + .delete(); + + if (deleted == 0) { + // dirtyVersion > snapshotVersion → 刷写期间有新变更 + // 释放认领,让 triggerFlush / Poller 重新处理 + // 同时重置 retryCount(本次成功说明通路正常) + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) + .set(VmMetadataDirtyVO_.retryCount, 0) + .set(VmMetadataDirtyVO_.nextRetryTime, null) + .update(); + + logger.debug(String.format("[MetadataDirty] vm[uuid:%s] has new changes during flush " + + "(snapshotVersion=%d), released for re-processing", vmUuid, snapshotVersion)); + } else { + logger.debug(String.format("[MetadataDirty] vm[uuid:%s] flush completed and dirty row removed", + vmUuid)); + } + } + }.execute(); + + // Δ-9:记录路径指纹(用于 PathDriftDetector 巡检) + savePathFingerprint(vmUuid); + } + + // ===================================================================== + // onFlushFailure — 刷写失败处理(指数退避 / 放弃) + // ===================================================================== + + /** + * 刷写失败后的处理。 + * + *

retryCount++ → 达到上限则标记 stale + 删除行(MetadataStaleRecoveryTask 接管); + * 未达上限则释放认领 + 指数退避。

+ * + *

C-RB-04: 退避参数来自 GlobalConfig,禁止硬编码。

+ *

C-SR-05: 重试耗尽时在 PathFingerprintVO 标记 lastFlushFailed=true。

+ */ + private void onFlushFailure(String vmUuid, ErrorCode error) { + VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + if (dirty == null) { + return; // VM 已销毁,FK CASCADE 已清理 + } + + int newRetryCount = dirty.getRetryCount() + 1; + int maxRetry = VmGlobalConfig.VM_METADATA_MAX_RETRY.value(Integer.class); + int baseDelay = VmGlobalConfig.VM_METADATA_RETRY_BASE_DELAY.value(Integer.class); + int maxExponent = VmGlobalConfig.VM_METADATA_RETRY_MAX_EXPONENT.value(Integer.class); + + if (newRetryCount >= maxRetry) { + // 达到上限 → 告警 + 标记 stale(C-SR-05:不再直接删除后静默放弃) + logger.error(String.format("[MetadataDirty] metadata update for vm[uuid:%s] failed " + + "after %d retries, marking as stale. MetadataStaleRecoveryTask will retry " + + "independently. Error: %s", vmUuid, newRetryCount, error)); + + // C-SR-05: 在 PathFingerprintVO 上标记 lastFlushFailed=true + SQL.New("UPDATE VmMetadataPathFingerprintVO " + + "SET lastFlushFailed = 1 WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmUuid) + .execute(); + + // 删除 dirty 行(释放 Poller 资源),stale 恢复由独立任务接管 + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .delete(); + return; + } + + // 未达上限 → 释放认领 + 指数退避(C-RB-04: 参数来自 GlobalConfig) + long delaySec = baseDelay * (1L << Math.min(newRetryCount, maxExponent)); + Timestamp nextRetry = Timestamp.from(Instant.now().plusSeconds(delaySec)); + + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) + .set(VmMetadataDirtyVO_.retryCount, newRetryCount) + .set(VmMetadataDirtyVO_.nextRetryTime, nextRetry) + .update(); + + logger.warn(String.format("[MetadataDirty] metadata update for vm[uuid:%s] failed " + + "(retry %d/%d), next retry at %s. Error: %s", + vmUuid, newRetryCount, maxRetry, nextRetry, error)); + } + + // ===================================================================== + // 辅助方法 + // ===================================================================== + + /** + * 释放 dirty 行的认领(managementNodeUuid 置 NULL)。 + */ + private void releaseClaim(String vmUuid) { + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) + .update(); + } + + /** + * 查找指定 VM dirty 行的 stale claim owner。 + * + *

若该 VM 的 dirty 行被某个 MN 认领,且 lastClaimTime 超过 staleThreshold, + * 则返回该 MN 的 UUID;否则返回 null。

+ * + * @param vmUuid 目标 VM UUID + * @param staleThreshold 认领超时阈值 + * @return stale claim owner 的 MN UUID,或 null + */ + private String findStaleClaimOwner(String vmUuid, Duration staleThreshold) { + Timestamp cutoff = Timestamp.from(Instant.now().minus(staleThreshold)); + return Q.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .notNull(VmMetadataDirtyVO_.managementNodeUuid) + .lt(VmMetadataDirtyVO_.lastClaimTime, cutoff) + .select(VmMetadataDirtyVO_.managementNodeUuid) + .findValue(); + } + + /** + * 记录 VM 的路径指纹(用于 MetadataPathDriftDetector 巡检)。 + * + *

每次 flush 成功后调用,INSERT or UPDATE VmMetadataPathFingerprintVO。 + * pathSnapshot 为当前 VM 所有 Volume + Snapshot 的 installPath 列表的 JSON。

+ * + *

pathSnapshot 构建使用 {@link MetadataPathSnapshotBuilder#buildPathJson}, + * 与 {@link MetadataPathDriftDetector} 巡检时使用完全相同的逻辑,确保一致性。

+ */ + private void savePathFingerprint(String vmUuid) { + // 构建当前路径快照 JSON + List volumes = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .orderBy(VolumeVO_.uuid, SimpleQuery.Od.ASC) + .list(); + + List snapshots; + if (volumes.isEmpty()) { + snapshots = new java.util.ArrayList<>(); + } else { + List volumeUuids = volumes.stream() + .map(VolumeVO::getUuid) + .collect(java.util.stream.Collectors.toList()); + snapshots = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.volumeUuid, volumeUuids) + .orderBy(VolumeSnapshotVO_.uuid, SimpleQuery.Od.ASC) + .list(); + } + + String pathJson = MetadataPathSnapshotBuilder.buildPathJson(volumes, snapshots); + + VmMetadataPathFingerprintVO fp = dbf.findByUuid(vmUuid, VmMetadataPathFingerprintVO.class); + if (fp == null) { + fp = new VmMetadataPathFingerprintVO(); + fp.setVmInstanceUuid(vmUuid); + fp.setPathSnapshot(pathJson); + fp.setLastFlushTime(new Timestamp(System.currentTimeMillis())); + fp.setLastFlushFailed(false); + fp.setStaleRecoveryCount(0); + dbf.persist(fp); + } else { + fp.setPathSnapshot(pathJson); + fp.setLastFlushTime(new Timestamp(System.currentTimeMillis())); + dbf.update(fp); + } + } + + /** + * 独立的僵尸 claim 清理任务(防御性措施,DP-05)。 + * + *

从 claimDirtyRows() 提取为独立低频任务,避免每 5s Poller 周期执行不必要的 + * write-intent 扫描。覆盖的场景:

+ *
    + *
  • MN 进程 hang 住(JVM 死锁 / 长 GC),心跳未失效但 flush 永久阻塞
  • + *
  • 网络分区导致目标 Agent 无响应,ChainTask 在 timeout 前持续持有 claim
  • + *
  • 极端:MN 已离线但 ManagementNodeVO 记录因 heartbeat 延迟尚未被清理
  • + *
+ * + *

C-CL-02: 阈值 15 分钟 > flush 最大超时(5min),安全余量充足。

+ */ + private void cleanupZombieClaims() { + long thresholdMinutes = VmGlobalConfig.VM_METADATA_ZOMBIE_CLAIM_THRESHOLD.value(Long.class); + int cleaned = SQL.New("UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = NULL, lastClaimTime = NULL " + + "WHERE managementNodeUuid IS NOT NULL " + + "AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL " + thresholdMinutes + " MINUTE") + .execute(); + + if (cleaned > 0) { + logger.info(String.format("[MetadataDirty] cleanupZombieClaims released %d zombie claim(s) " + + "(threshold=%d minutes)", cleaned, thresholdMinutes)); + } + } + + // ===================================================================== + // 升级全量刷新(§9.2) + // ===================================================================== + + /** + * 升级后全量刷新:为所有 UserVm 标脏,Poller 自动处理。 + * + *

§9.2: 使用 C-DM-01 兼容的 INSERT IGNORE + UPDATE 两步,keyset 分页。 + * storageStructureChange=1(C-SC-07:升级后无法判断存储拓扑是否变化)。

+ * + *

lastRefreshVersion 在全量刷新完成后写入(讨论 Δ-8): + * 若刷新过程中 MN 崩溃,重启后 lastRefreshVersion 仍为旧值 → 重新触发 → 幂等安全。

+ */ + private void submitFullRefresh(String currentVersion) { + logger.info(String.format("[MetadataDirty] metadata full refresh: starting for version %s", currentVersion)); + + int batchSize = VmGlobalConfig.VM_METADATA_UPGRADE_REFRESH_BATCH_SIZE.value(Integer.class); + String lastUuid = ""; + int totalProcessed = 0; + + while (true) { + // Step 1: INSERT IGNORE — 为尚无 dirty 行的 VM 创建新行 + SQL.New( + "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "SELECT v.uuid, 1, 1 FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + // Step 2: UPDATE — 已有 dirty 行的 VM 递增 dirtyVersion + 升级 storageStructureChange + SQL.New( + "UPDATE VmMetadataDirtyVO d " + + "INNER JOIN VmInstanceVO v ON d.vmInstanceUuid = v.uuid " + + "SET d.dirtyVersion = d.dirtyVersion + 1, " + + " d.storageStructureChange = 1 " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + // 更新 lastUuid 用于 keyset 分页 + List batch = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize", String.class) + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .list(); + + if (batch.isEmpty()) { + break; + } + + totalProcessed += batch.size(); + lastUuid = batch.get(batch.size() - 1); + } + + logger.info(String.format("[MetadataDirty] metadata full refresh: %d VMs processed for version %s", + totalProcessed, currentVersion)); + + // 更新 lastRefreshVersion — 必须在全量刷新完成后写入(讨论 Δ-8) + VmGlobalConfig.VM_METADATA_LAST_REFRESH_VERSION.updateValue(currentVersion); + } + + // ===================================================================== + // 功能开关 false→true:分批全量初始化(§9a.1) + // ===================================================================== + + /** + * 功能开关从 false 切换到 true 时,分批为尚无 dirty 行的 UserVm 创建 dirty 行。 + * + *

与 §9.2 升级全量刷新的区别:

+ *
    + *
  • 仅处理尚无 dirty 行的 VM(LEFT JOIN 排除已有行)
  • + *
  • storageStructureChange=0(首次初始化不涉及存储拓扑变更)
  • + *
  • 每批之间有延迟(防止 IO 风暴)
  • + *
  • 每轮重新检查开关状态(防御快速 toggle)
  • + *
+ */ + private void submitBatchInitialization() { + thdf.submit(new org.zstack.core.thread.Task() { + @Override + public String getName() { + return "metadata-batch-initialization"; + } + + @Override + public Void call() { + // 延迟 30s 启动,等待 Poller、ChainTask 线程池初始化完成 + try { + TimeUnit.SECONDS.sleep(30); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("[MetadataDirty] batch initialization startup delay interrupted"); + return null; + } + + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + // 延迟执行前再次检查,防止快速 toggle 后仍执行初始化 + logger.info("[MetadataDirty] vm.metadata.enabled toggled back to false before initialization, skip"); + return null; + } + + int batchSize = VmGlobalConfig.VM_METADATA_INIT_BATCH_SIZE.value(Integer.class); + long batchDelaySec = VmGlobalConfig.VM_METADATA_INIT_BATCH_DELAY.value(Long.class); + String lastUuid = ""; + int totalInitialized = 0; + + while (true) { + // 每轮检查开关状态,若已关闭则中止 + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + logger.info(String.format("[MetadataDirty] vm.metadata.enabled disabled during initialization, " + + "abort. initialized=%d", totalInitialized)); + break; + } + + // Keyset 分页查询尚无 dirty 行的 UserVm,INSERT IGNORE + int initialized = SQL.New( + "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "SELECT v.uuid, 1, 0 FROM VmInstanceVO v " + + "LEFT JOIN VmMetadataDirtyVO d ON v.uuid = d.vmInstanceUuid " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid AND d.vmInstanceUuid IS NULL " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + totalInitialized += initialized; + + // Q29 修复:lastUuid 基于 VmInstanceVO 全量 UUID 推进, + // 而非 INSERT 结果。当本批所有 VM 都已有 dirty 行时 INSERT IGNORE + // affected_rows=0,但后续批次可能还有未初始化的 VM。 + List batchUuids = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC", String.class) + .param("lastUuid", lastUuid) + .limit(batchSize) + .list(); + + if (batchUuids.isEmpty()) { + break; // 真正遍历完所有 VM + } + lastUuid = batchUuids.get(batchUuids.size() - 1); + + logger.info(String.format("[MetadataDirty] metadata initialization batch completed: " + + "%d VMs in this batch, %d total", initialized, totalInitialized)); + + // 批间延迟:等待 Poller 消化已有 dirty 行,避免瞬间堆积 + if (batchDelaySec > 0) { + try { + TimeUnit.SECONDS.sleep(batchDelaySec); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("[MetadataDirty] metadata initialization interrupted"); + break; + } + } + } + + logger.info(String.format("[MetadataDirty] metadata initialization complete: %d VMs total", + totalInitialized)); + return null; + } + }); + } + + // ===================================================================== + // 功能开关 true→false:清理 PathFingerprint(§9a.2 讨论 Δ-10) + // ===================================================================== + + /** + * 功能关闭时异步批量删除所有 VmMetadataPathFingerprintVO 行。 + * + *

§9a.2 讨论 Δ-10:功能关闭期间存储拓扑可能发生变更, + * 重新启用时旧指纹与实际拓扑不一致,会导致路径巡检产生大量误报。 + * 清理采用 keyset 分页异步删除(每批 1000 行),不阻塞 GlobalConfig 变更回调。

+ */ + private void cleanupPathFingerprints() { + thdf.submit(new org.zstack.core.thread.Task() { + @Override + public String getName() { + return "metadata-cleanup-path-fingerprints"; + } + + @Override + public Void call() { + String lastUuid = ""; + int totalDeleted = 0; + int batchSize = 1000; + + while (true) { + List batch = SQL.New( + "SELECT vmInstanceUuid FROM VmMetadataPathFingerprintVO " + + "WHERE vmInstanceUuid > :lastUuid " + + "ORDER BY vmInstanceUuid ASC LIMIT :batchSize", String.class) + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .list(); + + if (batch.isEmpty()) { + break; + } + + int deleted = SQL.New("DELETE FROM VmMetadataPathFingerprintVO " + + "WHERE vmInstanceUuid IN (:uuids)") + .param("uuids", batch) + .execute(); + totalDeleted += deleted; + lastUuid = batch.get(batch.size() - 1); + } + + if (totalDeleted > 0) { + logger.info(String.format("[MetadataDirty] cleaned up %d PathFingerprint rows " + + "after metadata feature disabled", totalDeleted)); + } + return null; + } + }); + } + + // ===================================================================== + // 升级后全量刷新调度(§9.1) + // ===================================================================== + + /** + * 升级后自动检测是否需要全量刷新。 + * + *

§9.1: 比较 {@code dbf.getDbVersion()} 与 {@code VM_METADATA_LAST_REFRESH_VERSION}, + * 若不一致则在延迟后执行 {@link #submitFullRefresh(String)}。

+ * + *

延迟原因:升级后多个 MN 同时启动,仅需一个 MN 执行全量刷新。 + * 通过 {@code VM_METADATA_UPGRADE_REFRESH_DELAY}(默认 600s)延迟 + 执行前 re-check + * 实现"最终只有一个 MN 执行"的效果(best-effort, 非 leader election)。

+ * + *

M3 recent-nodeLeft 防护:延迟到期后若近 15 分钟内发生过 nodeLeft, + * 说明集群可能不稳定,递归 reschedule 以避免在 MN 重新平衡期间执行全量刷新。

+ */ + private void scheduleUpgradeRefreshIfNeeded() { + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + + String currentVersion = dbf.getDbVersion(); + String lastRefreshVersion = VmGlobalConfig.VM_METADATA_LAST_REFRESH_VERSION.value(String.class); + + if (currentVersion.equals(lastRefreshVersion)) { + logger.debug("[MetadataDirty] DB version matches lastRefreshVersion, no upgrade refresh needed"); + return; + } + + long delaySec = VmGlobalConfig.VM_METADATA_UPGRADE_REFRESH_DELAY.value(Long.class); + logger.info(String.format("[MetadataDirty] DB version %s != lastRefreshVersion %s, " + + "scheduling upgrade refresh after %ds delay", currentVersion, lastRefreshVersion, delaySec)); + + thdf.submitTimeoutTask(() -> { + // Re-check: version may have changed, or feature may be disabled + if (!VmGlobalConfig.VM_METADATA.value(Boolean.class)) { + return; + } + String recheckVersion = dbf.getDbVersion(); + if (!recheckVersion.equals(currentVersion)) { + logger.warn("[MetadataDirty] DB version changed during upgrade refresh delay, skip"); + return; + } + String recheckLastRefresh = VmGlobalConfig.VM_METADATA_LAST_REFRESH_VERSION.value(String.class); + if (recheckVersion.equals(recheckLastRefresh)) { + logger.info("[MetadataDirty] another MN already completed upgrade refresh, skip"); + return; + } + + // M3 recent-nodeLeft check: if nodeLeft within last 15 min, reschedule + long recentNodeLeftWindowMs = 15L * 60 * 1000; + if (System.currentTimeMillis() - lastNodeLeftTimestamp < recentNodeLeftWindowMs) { + logger.info("[MetadataDirty] recent nodeLeft detected, rescheduling upgrade refresh"); + scheduleUpgradeRefreshIfNeeded(); // re-enter with fresh delay + return; + } + + submitFullRefresh(recheckVersion); + }, TimeUnit.SECONDS, delaySec); + } + + /** + * 启动僵尸 claim 清理定时任务(60s 间隔)。 + */ + private synchronized void startZombieCleanupTask() { + if (zombieCleanupFuture != null) { + zombieCleanupFuture.cancel(false); + } + zombieCleanupFuture = thdf.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return 60; + } + + @Override + public String getName() { + return "vm-metadata-zombie-claim-cleanup"; + } + + @Override + public void run() { + cleanupZombieClaims(); + } + }); + logger.info("[MetadataDirty] zombie claim cleanup task started (interval=60s)"); + } + + /** + * 停止僵尸 claim 清理定时任务。 + */ + private synchronized void stopZombieCleanupTask() { + if (zombieCleanupFuture != null) { + zombieCleanupFuture.cancel(false); + zombieCleanupFuture = null; + logger.info("[MetadataDirty] zombie claim cleanup task stopped"); + } + } +} diff --git a/conf/globalConfig/vm.xml b/conf/globalConfig/vm.xml index 8563169b335..bae6e369e5f 100755 --- a/conf/globalConfig/vm.xml +++ b/conf/globalConfig/vm.xml @@ -317,4 +317,68 @@ java.lang.Boolean false + + + vm + deletion.gcInterval + update vm metadata interval + java.lang.Long + 30 + + + + vm + vm.metadata + save vm metadata + java.lang.Boolean + false + + + + vm + vm.metadata.ps.maxConcurrent + Max concurrent metadata writes per primary storage per MN. In dual-MN the actual global concurrency per PS = 2 × this value. + java.lang.Integer + 5 + + + + vm + vm.metadata.global.maxConcurrent + Max concurrent VM metadata updates globally per MN. In dual-MN the actual global concurrency = 2 × this value. + java.lang.Integer + 10 + + + + vm + vm.metadata.gc.initialDelaySec + Initial GC delay in seconds after API success. The first metadata update attempt happens after this delay. + java.lang.Integer + 10 + + + + vm + vm.metadata.maxRetry + Max retry count before giving up metadata flush. After this many failed attempts with exponential backoff, the dirty row is deleted and will auto-retry on the next API that modifies this VM. + java.lang.Integer + 5 + + + + vm + vm.metadata.dirty.pollIntervalSec + Dirty poller interval in seconds. The poller acts as a safety net to process dirty rows that were not handled by the immediate triggerFlush path (e.g., rows in backoff, orphaned rows after MN crash). + java.lang.Long + 5 + + + + vm + vm.metadata.dirty.batchSize + Max dirty rows to claim per poller cycle. Controls the batch size of CAS claim in each poller round. + java.lang.Integer + 20 + diff --git a/conf/serviceConfig/primaryStorage.xml b/conf/serviceConfig/primaryStorage.xml index 337ce4eaac3..06f4d94bc07 100755 --- a/conf/serviceConfig/primaryStorage.xml +++ b/conf/serviceConfig/primaryStorage.xml @@ -84,4 +84,7 @@ org.zstack.header.storage.primary.APIAddStorageProtocolMsg + + org.zstack.header.storage.primary.APIRegisterVmInstanceMsg + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 20e094378aa..0d3e7138af7 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 @@ -267,4 +268,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ee1696a77286d6cc5e37e85797eeb381905fe19c Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:49:01 +0800 Subject: [PATCH 05/10] [vm-metadata]: storage layer handlers (Local/NFS/KVM) - PrimaryStorageBase: metadata read/write/cleanup message handling (+388) - KVMHost: metadata update/read on hypervisor via agent commands - KVMAgentCommands: metadata read/write command structures - KVMConstant: metadata agent paths - LocalStorageBase/HypervisorBackend/KvmBackend: local storage metadata - NfsPrimaryStorage/Backend/KVMBackend: NFS storage metadata - MetadataStorageHandler implementation for local and NFS paths Resolves: ZSV-10000 Part: 01c --- .../java/org/zstack/kvm/KVMAgentCommands.java | 16 + .../main/java/org/zstack/kvm/KVMConstant.java | 3 + .../src/main/java/org/zstack/kvm/KVMHost.java | 87 ++++ .../primary/local/LocalStorageBase.java | 48 +++ .../local/LocalStorageHypervisorBackend.java | 4 + .../primary/local/LocalStorageKvmBackend.java | 32 +- .../primary/nfs/NfsPrimaryStorage.java | 60 ++- .../primary/nfs/NfsPrimaryStorageBackend.java | 4 + .../nfs/NfsPrimaryStorageKVMBackend.java | 34 +- .../storage/primary/PrimaryStorageBase.java | 388 ++++++++++++++++++ 10 files changed, 666 insertions(+), 10 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 92e76ede2c5..51e3c4f1dee 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -4716,4 +4716,20 @@ public void setMemoryUsage(long memoryUsage) { this.memoryUsage = memoryUsage; } } + + public static class WriteVmInstanceMetadataCmd extends AgentCommand { + public String metadata; + public String metadataPath; + } + + public static class WriteVmInstanceMetadataRsp extends AgentResponse { + } + + public static class ReadVmInstanceMetadataCmd extends AgentCommand { + public String metadataPath; + } + + public static class ReadVmInstanceMetadataRsp extends AgentResponse { + public String metadata; + } } 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..6c845676ca8 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -86,6 +86,9 @@ public interface KVMConstant { String CLEAN_FIRMWARE_FLASH = "/clean/firmware/flash"; String FSTRIM_VM_PATH = "/vm/fstrim"; + String WRITE_VM_INSTANCE_METADATA_PATH = "/vm/metadata/write"; + String READ_VM_INSTANCE_METADATA_PATH = "/vm/metadata/read"; + String ISO_TO = "kvm.isoto"; String ANSIBLE_PLAYBOOK_NAME = "kvm.py"; String ANSIBLE_MODULE_PATH = "ansible/kvm"; 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..86a19c60b2f 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 writeVmInstanceMetadataPath; + private String readVmInstanceMetadataPath; 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.WRITE_VM_INSTANCE_METADATA_PATH); + writeVmInstanceMetadataPath = ub.build().toString(); + + ub = UriComponentsBuilder.fromHttpUrl(baseUrl); + ub.path(KVMConstant.READ_VM_INSTANCE_METADATA_PATH); + readVmInstanceMetadataPath = ub.build().toString(); } static { @@ -738,6 +748,10 @@ protected void handleLocalMessage(Message msg) { handle((GetFileDownloadProgressMsg) msg); } else if (msg instanceof RestartKvmAgentMsg) { handle((RestartKvmAgentMsg) msg); + } else if (msg instanceof UpdateVmInstanceMetadataOnHypervisorMsg) { + handle((UpdateVmInstanceMetadataOnHypervisorMsg) msg); + } else if (msg instanceof ReadVmInstanceMetadataOnHypervisorMsg) { + handle((ReadVmInstanceMetadataOnHypervisorMsg) msg); } else { super.handleLocalMessage(msg); } @@ -7309,4 +7323,77 @@ public void fail(ErrorCode errorCode) { } }); } + + private void handle(UpdateVmInstanceMetadataOnHypervisorMsg msg) { + inQueue().name(String.format("update-vmInstance-metadata-on-host-%s", self.getUuid())) + .asyncBackup(msg) + .run(chain -> updateVmInstanceMetadata(msg, new NoErrorCompletion(chain) { + @Override + public void done() { + chain.next(); + } + })); + } + + private void updateVmInstanceMetadata(final UpdateVmInstanceMetadataOnHypervisorMsg msg, final NoErrorCompletion completion) { + UpdateVmInstanceMetadataOnHypervisorReply reply = new UpdateVmInstanceMetadataOnHypervisorReply(); + + checkStatus(); + WriteVmInstanceMetadataCmd cmd = new WriteVmInstanceMetadataCmd(); + cmd.metadata = msg.getMetadata(); + cmd.metadataPath = msg.getMetadataPath(); + new Http<>(writeVmInstanceMetadataPath, cmd, WriteVmInstanceMetadataRsp.class).call(new ReturnValueCompletion(msg) { + @Override + public void success(WriteVmInstanceMetadataRsp ret) { + if (!ret.isSuccess()) { + reply.setError(operr("operation error, because:%s", ret.getError())); + } + bus.reply(msg, reply); + completion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + completion.done(); + } + }); + } + + private void handle(ReadVmInstanceMetadataOnHypervisorMsg msg) { + inQueue().name(String.format("readVmInstanceMetadata-on-host-%s", self.getUuid())) + .asyncBackup(msg) + .run(chain -> readVmInstanceMetadata(msg, new NoErrorCompletion(chain) { + @Override + public void done() { + chain.next(); + } + })); + } + + private void readVmInstanceMetadata(final ReadVmInstanceMetadataOnHypervisorMsg msg, final NoErrorCompletion completion) { + checkStatus(); + ReadVmInstanceMetadataOnHypervisorReply reply = new ReadVmInstanceMetadataOnHypervisorReply(); + ReadVmInstanceMetadataCmd cmd = new ReadVmInstanceMetadataCmd(); + cmd.metadataPath = msg.getMetadataPath(); + new Http<>(readVmInstanceMetadataPath, cmd, ReadVmInstanceMetadataRsp.class).call(new ReturnValueCompletion(msg) { + @Override + public void success(ReadVmInstanceMetadataRsp rsp) { + if (!rsp.isSuccess()) { + reply.setError(operr("operation error, because:%s", rsp.getError())); + } + reply.setMetadata(rsp.metadata); + bus.reply(msg, reply); + completion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + completion.done(); + } + }); + } } 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..792cd13017e 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 @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.compute.host.VolumeMigrationTargetHostFilter; +import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.EventFacade; @@ -902,6 +903,8 @@ public void handleLocalMessage(Message msg) { handle((CommitVolumeSnapshotOnPrimaryStorageMsg) msg); } else if (msg instanceof PullVolumeSnapshotOnPrimaryStorageMsg) { handle((PullVolumeSnapshotOnPrimaryStorageMsg) msg); + } else if (msg instanceof UpdateVmInstanceMetadataOnPrimaryStorageMsg) { + handle((UpdateVmInstanceMetadataOnPrimaryStorageMsg) msg); } else { super.handleLocalMessage(msg); } @@ -3329,4 +3332,49 @@ public void fail(ErrorCode errorCode) { public static class LocalStoragePhysicalCapacityUsage extends PrimaryStorageBase.PhysicalCapacityUsage { public long localStorageUsedSize; } + + private void handle(final UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + // Layer 3: PS-level concurrency control (§4) + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return "update-metadata-on-ps-" + self.getUuid(); + } + + @Override + public int getSyncLevel() { + return VmGlobalConfig.VM_METADATA_PS_MAX_CONCURRENT.value(Integer.class); + } + + @Override + public void run(SyncTaskChain chain) { + doHandleUpdateMetadata(msg); + chain.next(); + } + + @Override + public String getName() { + return "update-metadata-on-ps-" + self.getUuid(); + } + }); + } + + private void doHandleUpdateMetadata(final UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + final String hostUuid = getHostUuidByResourceUuid(msg.getRootVolumeUuid()); + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid); + LocalStorageHypervisorBackend bkd = f.getHypervisorBackend(self); + bkd.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(UpdateVmInstanceMetadataOnPrimaryStorageReply returnValue) { + bus.reply(msg, returnValue); + } + + @Override + public void fail(ErrorCode errorCode) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } } 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..7e85d562d8c 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.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.*; import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageMsg; import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageReply; @@ -121,4 +123,6 @@ 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); } 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..7da947744b7 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,10 +43,8 @@ import org.zstack.header.storage.backup.*; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.snapshot.*; +import org.zstack.header.vm.*; 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.volume.*; import org.zstack.identity.AccountManager; import org.zstack.kvm.*; @@ -70,6 +68,7 @@ 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.header.vm.VmInstanceConstant.VM_META_SUFFIX; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; @@ -3797,4 +3796,31 @@ public void fail(ErrorCode errorCode) { } }); } + + @Override + void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + String installPath = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, msg.getRootVolumeUuid()).select(VolumeVO_.installPath).findValue(); + // /vms_ds/rootVolumes/acct-36c27e8ff05c4780bf6d2fa65700f22e/vol-829a91b68e794a03865eab8a5918600a/snapshots/f2c31aeede604917aa8cee24848d8bfa.qcow2 + // /vms_ds/rootVolumes/acct-36c27e8ff05c4780bf6d2fa65700f22e/vol-829a91b68e794a03865eab8a5918600a/829a91b68e794a03865eab8a5918600a.qcow2 + + String path = installPath.replaceFirst("^(.+/vol-[^/]+/).*$", "$1"); + String metadataPath = String.format("%s%s", path, VM_META_SUFFIX); + + UpdateVmInstanceMetadataOnHypervisorMsg umsg = new UpdateVmInstanceMetadataOnHypervisorMsg(); + umsg.setMetadata(msg.getMetadata()); + umsg.setMetadataPath(metadataPath); + umsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(umsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(umsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + if (!r.isSuccess()) { + reply.setError(Platform.operr("failed to update vm[uuid=%s] on hypervisor.", self.getUuid()) + .withCause(r.getError())); + } + bus.reply(msg, reply); + } + }); + } } 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..f179dacfeb9 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 @@ -40,10 +40,9 @@ import org.zstack.header.storage.snapshot.ShrinkVolumeSnapshotOnPrimaryStorageMsg; import org.zstack.header.storage.snapshot.VolumeSnapshotConstant; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.header.vm.*; 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.volume.*; import org.zstack.kvm.*; import org.zstack.storage.primary.*; @@ -131,6 +130,8 @@ protected void handleLocalMessage(Message msg) { handle((CommitVolumeSnapshotOnPrimaryStorageMsg) msg); } else if (msg instanceof PullVolumeSnapshotOnPrimaryStorageMsg) { handle((PullVolumeSnapshotOnPrimaryStorageMsg) msg); + } else if (msg instanceof UpdateVmInstanceMetadataOnPrimaryStorageMsg) { + handle((UpdateVmInstanceMetadataOnPrimaryStorageMsg) msg); } else { super.handleLocalMessage(msg); } @@ -1924,4 +1925,57 @@ private String getHostUuidFromVolume(String volumeUuid) { return hostUuid; } + + protected void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + // Layer 3: PS-level concurrency control (§4) + // 同一 MN 上同一 PS 最多 N 个并发元数据写入 + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return "update-metadata-on-ps-" + self.getUuid(); + } + + @Override + public int getSyncLevel() { + return VmGlobalConfig.VM_METADATA_PS_MAX_CONCURRENT.value(Integer.class); + } + + @Override + public void run(SyncTaskChain chain) { + doHandleUpdateMetadata(msg); + chain.next(); + } + + @Override + public String getName() { + return "update-metadata-on-ps-" + self.getUuid(); + } + }); + } + + private void doHandleUpdateMetadata(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + 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); + return; + } + + final NfsPrimaryStorageBackend backend = getUsableBackend(); + + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(UpdateVmInstanceMetadataOnPrimaryStorageReply 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..a19f2d1d38e 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.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.VolumeStats; import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageMsg; import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageReply; @@ -91,6 +93,8 @@ public interface NfsPrimaryStorageBackend { void updateMountPoint(PrimaryStorageInventory pinv, String clusterUuid, String oldMountPoint, String newMountPoint, Completion completion); + void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg 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 93d3d7aab99..e3ec8ac8c35 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 @@ -35,10 +35,7 @@ import org.zstack.header.storage.primary.*; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; -import org.zstack.header.vm.VmInstanceSpec; -import org.zstack.header.vm.VmInstanceState; -import org.zstack.header.vm.VmInstanceVO; -import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.*; import org.zstack.header.volume.*; import org.zstack.identity.AccountManager; import org.zstack.kvm.*; @@ -67,6 +64,7 @@ import static java.lang.Integer.min; import static org.zstack.core.Platform.operr; import static org.zstack.core.Platform.touterr; +import static org.zstack.header.vm.VmInstanceConstant.VM_META_SUFFIX; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; public class NfsPrimaryStorageKVMBackend implements NfsPrimaryStorageBackend, @@ -2051,4 +2049,32 @@ public void run(MessageReply r) { } }); } + + public void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + UpdateVmInstanceMetadataOnHypervisorMsg umsg = new UpdateVmInstanceMetadataOnHypervisorMsg(); + umsg.setMetadata(msg.getMetadata()); + umsg.setHostUuid(hostUuid); + + String installPath = Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, msg.getRootVolumeUuid()) + .select(VolumeVO_.installPath) + .findValue(); + String path = installPath.replaceFirst("^(.+/vol-[^/]+/).*$", "$1"); + String metadataPath = String.format("%s%s", path, VM_META_SUFFIX); + umsg.setMetadataPath(metadataPath); + + bus.makeTargetServiceIdByResourceUuid(umsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(umsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + if (!r.isSuccess()) { + reply.setError(Platform.operr("failed to update vm[uuid=%s] metadata on hypervisor via host[uuid:%s]", + msg.getVmInstanceUuid(), hostUuid) + .withCause(r.getError())); + } + completion.success(reply); + } + }); + } } 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..869e7133c26 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java @@ -14,6 +14,8 @@ import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.cloudbus.EventFacade; import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.core.config.GlobalConfig; +import org.zstack.core.config.GlobalConfigDefinition; import org.zstack.core.db.*; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.core.errorcode.ErrorFacade; @@ -27,6 +29,7 @@ import org.zstack.core.trash.TrashType; import org.zstack.core.workflow.FlowChainBuilder; import org.zstack.core.workflow.ShareFlow; +import org.zstack.core.workflow.ShareFlowChain; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.core.*; import org.zstack.header.core.trash.CleanTrashResult; @@ -49,16 +52,24 @@ import org.zstack.header.storage.primary.PrimaryStorageCanonicalEvent.PrimaryStorageDeletedData; import org.zstack.header.storage.primary.PrimaryStorageCanonicalEvent.PrimaryStorageStatusChangedData; import org.zstack.header.storage.snapshot.*; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.tag.TagDefinition; import org.zstack.header.vm.*; import org.zstack.header.volume.*; +import org.zstack.resourceconfig.BindResourceConfig; import org.zstack.storage.volume.VolumeUtils; +import org.zstack.tag.SystemTag; +import org.zstack.utils.BeanUtils; import org.zstack.utils.CollectionDSL; import org.zstack.utils.DebugUtils; import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import javax.persistence.LockModeType; import javax.persistence.TypedQuery; +import java.lang.reflect.Field; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -177,6 +188,8 @@ public void setNewAdded(boolean newAdded) { protected abstract void handle(GetVolumeSnapshotEncryptedOnPrimaryStorageMsg msg); + protected abstract void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg); + public PrimaryStorageBase(PrimaryStorageVO self) { this.self = self; } @@ -935,6 +948,10 @@ protected void handleApiMessage(APIMessage msg) { handle((APICleanUpStorageTrashOnPrimaryStorageMsg) msg); } else if (msg instanceof APIAddStorageProtocolMsg) { handle((APIAddStorageProtocolMsg) msg); + } else if (msg instanceof APIRegisterVmInstanceMsg) { + handle((APIRegisterVmInstanceMsg) msg); + } else if (msg instanceof APIGetVmInstanceMetadataFromPrimaryStorageMsg) { + handle((APIGetVmInstanceMetadataFromPrimaryStorageMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -1812,4 +1829,375 @@ 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(APIRegisterVmInstanceMsg msg) { + APIRegisterVmInstanceReply event = new APIRegisterVmInstanceReply(msg.getId()); + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return String.format("register-vm-from-%s", msg.getMetadataPath()); + } + + @Override + public void run(SyncTaskChain chain) { + registerVmInstance(msg, new ReturnValueCompletion(chain, msg) { + @Override + public void success(VmInstanceInventory vmInstanceInventory) { + event.setInventory(vmInstanceInventory); + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getName() { + return String.format("register-vm-from-%s", msg.getMetadataPath()); + } + }); + } + + private void registerVmInstance(APIRegisterVmInstanceMsg msg, ReturnValueCompletion completion) { + FlowChain chain = new ShareFlowChain(); + chain.setName("register-vm-from-metadata"); + chain.then(new ShareFlow() { + VmMetadata vmMetadata; + VmInstanceInventory vmInstanceInventory; + + @Override + public void setup() { + flow(new NoRollbackFlow() { + String __name__ = "read-metadata"; + + @Override + public void run(FlowTrigger trigger, Map data) { + ReadVmInstanceMetadataOnHypervisorMsg umsg = new ReadVmInstanceMetadataOnHypervisorMsg(); + umsg.setHostUuid(msg.getHostUuid()); + umsg.setMetadataPath(msg.getMetadataPath()); + bus.makeTargetServiceIdByResourceUuid(umsg, HostConstant.SERVICE_ID, msg.getHostUuid()); + bus.send(umsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + trigger.fail(operr("failed to update vm[uuid=%s] on hypervisor.", + self.getUuid()).withCause(r.getError())); + return; + } + ReadVmInstanceMetadataOnHypervisorReply reply = r.castReply(); + vmMetadata = JSONObjectUtil.toObject(reply.getMetadata(), VmMetadata.class); + trigger.next(); + } + }); + } + }); + + flow(new NoRollbackFlow() { + String __name__ = "register-volume"; + + @Override + public void run(FlowTrigger trigger, Map data) { + List volumesString = vmMetadata.volumeVOs; + + List volumes = new ArrayList<>(); + volumesString.forEach(v -> volumes.add(JSONObjectUtil.toObject(v, VolumeVO.class))); + + List newVolumes = new ArrayList<>(); + volumes.forEach(v -> { + VolumeVO vo = new VolumeVO(); +// vo.setRootImageUuid(vo.getRootImageUuid()); + vo.setAccountUuid(msg.getSession().getAccountUuid()); + vo.setPrimaryStorageUuid(msg.getPrimaryStorageUuid()); + vo.setInstallPath(v.getInstallPath()); + + vo.setCreateDate(v.getCreateDate()); + vo.setDescription(v.getDescription()); + vo.setName(v.getName()); + vo.setSize(v.getSize()); + vo.setActualSize(v.getActualSize()); + vo.setState(v.getState()); + vo.setUuid(v.getUuid()); + vo.setVmInstanceUuid(v.getVmInstanceUuid()); + vo.setType(v.getType()); + vo.setCreateDate(v.getCreateDate()); + vo.setLastOpDate(v.getLastOpDate()); + vo.setDeviceId(v.getDeviceId()); + vo.setStatus(v.getStatus()); + vo.setFormat(v.getFormat()); + vo.setShareable(v.isShareable()); + vo.setVolumeQos(v.getVolumeQos()); + vo.setLastDetachDate(v.getLastDetachDate()); + vo.setLastVmInstanceUuid(v.getLastVmInstanceUuid()); + vo.setLastAttachDate(v.getLastAttachDate()); + vo.setProtocol(v.getProtocol()); + newVolumes.add(vo); + }); + dbf.persistCollection(newVolumes); + trigger.next(); + } + }); + + flow(new NoRollbackFlow() { + String __name__ = "register-snapshot"; + + @Override + public void run(FlowTrigger trigger, Map data) { + // 快照 + vmMetadata.volumeSnapshots.forEach((volumeUuid, snapshotList) -> { + // 一个 volume 有多个快照树 + // key = treeuuid + // value = snapshosts + Map> snapshotsByTreeUuid = new HashMap<>(); + snapshotList.forEach(snapshot -> { + VolumeSnapshotInventory inv = JSONObjectUtil.toObject(snapshot, VolumeSnapshotInventory.class); + if (snapshotsByTreeUuid.containsKey(inv.getTreeUuid())) { + snapshotsByTreeUuid.get(inv.getTreeUuid()).add(inv); + } else { + snapshotsByTreeUuid.put(inv.getTreeUuid(), new ArrayList<>()); + snapshotsByTreeUuid.get(inv.getTreeUuid()).add(inv); + } + }); + + // 遍历每一颗树 + snapshotsByTreeUuid.forEach((treeUuid, snapshots) -> { + //构建快照树 + VolumeSnapshotTree tree = VolumeSnapshotTree.fromInventories(snapshots); + // 层级遍历 快照 + List levelOrderTraversals = tree.levelOrderTraversal(); + // 判断当前树有没有 latest 节点 + boolean treeIsCurrent = levelOrderTraversals.stream().anyMatch(VolumeSnapshotInventory::isLatest); + + // 先创建快照树,VolumeSnapshotVO 外键依赖 VolumeSnapshotTreeVO + VolumeSnapshotTreeVO newTree = new VolumeSnapshotTreeVO(); + newTree.setCurrent(treeIsCurrent); + newTree.setVolumeUuid(volumeUuid); + newTree.setUuid(treeUuid); + newTree.setStatus(VolumeSnapshotTreeStatus.Completed); + dbf.persist(newTree); + + // 按照层级遍历的快照构建VolumeSnapshotTreeVO + levelOrderTraversals.forEach(snapshot -> { + VolumeSnapshotVO vo = new VolumeSnapshotVO(); + vo.setPrimaryStorageUuid(msg.getPrimaryStorageUuid()); + vo.setPrimaryStorageInstallPath(snapshot.getPrimaryStorageInstallPath()); + + vo.setName(snapshot.getName()); + vo.setCreateDate(snapshot.getCreateDate()); + vo.setDescription(snapshot.getDescription()); + vo.setLastOpDate(snapshot.getLastOpDate()); + vo.setParentUuid(snapshot.getParentUuid()); + vo.setState(VolumeSnapshotState.valueOf(snapshot.getState())); + vo.setType(snapshot.getType()); + vo.setVolumeUuid(snapshot.getVolumeUuid()); + vo.setFormat(snapshot.getFormat()); + vo.setUuid(snapshot.getUuid()); + vo.setStatus(VolumeSnapshotStatus.valueOf(snapshot.getStatus())); + vo.setLatest(snapshot.isLatest()); + vo.setSize(snapshot.getSize()); + vo.setVolumeType(snapshot.getVolumeType()); + vo.setTreeUuid(snapshot.getTreeUuid()); + vo.setDistance(snapshot.getDistance()); + dbf.persist(vo); + }); + }); + }); + + // 快照组 + List newGroups = new ArrayList<>(); + vmMetadata.volumeSnapshotGroupVO.forEach(group -> { + VolumeSnapshotGroupVO vo = JSONObjectUtil.toObject(group, VolumeSnapshotGroupVO.class); + vo.setAccountUuid(msg.getSession().getAccountUuid()); + newGroups.add(vo); + }); + dbf.persistCollection(newGroups); + + // 快照组ref + List newGroupRefs = new ArrayList<>(); + vmMetadata.volumeSnapshotGroupRefVO.forEach(group -> { + VolumeSnapshotGroupRefVO vo = JSONObjectUtil.toObject(group, VolumeSnapshotGroupRefVO.class); + newGroupRefs.add(vo); + }); + dbf.persistCollection(newGroupRefs); + + trigger.next(); + } + }); + + flow(new NoRollbackFlow() { + String __name__ = "register-vmInstance"; + + @Override + public void run(FlowTrigger trigger, Map data) { + VmInstanceVO metaVm = JSONObjectUtil.toObject(vmMetadata.vmInstanceVO, VmInstanceVO.class); + VmInstanceVO newVm = new VmInstanceVO(); + + newVm.setClusterUuid(msg.getClusterUuid()); + newVm.setHostUuid(msg.getHostUuid()); + // 寻找有没有cache的tag lv 构建imageCache +// newVm.setImageUuid(); + + newVm.setUuid(metaVm.getUuid()); + newVm.setName(metaVm.getName()); + newVm.setDescription(metaVm.getDescription()); + newVm.setType(metaVm.getType()); + newVm.setHypervisorType(metaVm.getHypervisorType()); + newVm.setCreateDate(metaVm.getCreateDate()); + newVm.setLastOpDate(metaVm.getLastOpDate()); + newVm.setState(metaVm.getState()); + newVm.setRootVolumeUuid(metaVm.getRootVolumeUuid()); + newVm.setInternalId(metaVm.getInternalId()); + newVm.setCpuNum(metaVm.getCpuNum()); + newVm.setCpuSpeed(metaVm.getCpuSpeed()); + newVm.setMemorySize(metaVm.getMemorySize()); + newVm.setReservedMemorySize(metaVm.getReservedMemorySize()); + newVm.setAllocatorStrategy(metaVm.getAllocatorStrategy()); + newVm.setPlatform(metaVm.getPlatform()); + newVm.setArchitecture(metaVm.getArchitecture()); + newVm.setGuestOsType(metaVm.getGuestOsType()); + dbf.persist(newVm); + vmInstanceInventory = VmInstanceInventory.valueOf(newVm); + trigger.next(); +// List vmSystemTags = vmMetadata.vmSystemTags; +// List vmResourceConfigs = vmMetadata.vmResourceConfigs; +// +// try { +// List systemTags = getResourceSystemTagFromSystem(VmInstanceVO.class.getSimpleName()); +// List resourceConfigs = getResourceConfigFromSystem(VmInstanceVO.class.getSimpleName()); +// +// List tagVOS = new ArrayList<>(); +// vmSystemTags.forEach(tag -> { +// List info = asList(tag.split("_")); +// String t = info.get(0); +// Boolean inherent = Boolean.valueOf(info.get(1)); +// String type = info.get(2); +// systemTags.forEach(it -> { +// if (!it.isMatch(t)) { +// return; +// } +// SystemTagVO vo = new SystemTagVO(); +// vo.setTag(t); +// vo.setType(TagType.valueOf(type)); +// vo.setInherent(inherent); +// vo.setResourceType(VmInstanceVO.class.getSimpleName()); +// vo.setResourceUuid(newVm.getUuid()); +// tagVOS.add(vo); +// }); +// }); +// +// List configVOS = new ArrayList<>(); +// vmResourceConfigs.forEach(tag -> { +// List info = asList(tag.split("_")); +// String identity = info.get(0); +// String value = info.get(1); +// resourceConfigs.forEach(it -> { +// if (it.getIdentity() == identity) { +// return; +// } +// ResourceConfigVO vo = new ResourceConfigVO(); +// vo.setCategory(identity); +// vo.setName(identity); +// vo.setValue(value); +// vo.setResourceType(VmInstanceVO.class.getSimpleName()); +// vo.setResourceUuid(newVm.getUuid()); +// configVOS.add(vo); +// }); +// }); +// } catch (IllegalAccessException | InstantiationException e) { +// throw new RuntimeException(e); +// } + } + }); + + done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(vmInstanceInventory); + } + }); + + error(new FlowErrorHandler(msg) { + @Override + public void handle(ErrorCode errCode, Map data) { + completion.fail(errCode); + } + }); + } + }).start(); + } + + private List getResourceSystemTagFromSystem(String resourceType) throws IllegalAccessException, InstantiationException { + List systemTags = new ArrayList<>(); + + Set> classes = BeanUtils.reflections.getTypesAnnotatedWith(TagDefinition.class); + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (!SystemTag.class.isAssignableFrom(field.getType())) { + continue; + } + + SystemTag systemTag = (SystemTag) field.get(clazz.newInstance()); + + if (resourceType.equals(systemTag.getResourceClass().getName())) { + systemTags.add(systemTag); + } + } + } + return systemTags; + } + + private List getResourceConfigFromSystem(String resourceType) throws IllegalAccessException, InstantiationException { + List globalConfigs = new ArrayList<>(); + + Set> classes = BeanUtils.reflections.getTypesAnnotatedWith(GlobalConfigDefinition.class); + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (!GlobalConfig.class.isAssignableFrom(field.getType())) { + continue; + } + GlobalConfig globalConfig = (GlobalConfig) field.get(clazz.newInstance()); + + BindResourceConfig bindResourceConfig = field.getAnnotation(BindResourceConfig.class); + if (bindResourceConfig == null) { + continue; + } + + List bindResourceConfigs = Arrays.stream(bindResourceConfig.value()).map(Class::getName).collect(Collectors.toList()); + + if (bindResourceConfigs.contains(resourceType)) { + globalConfigs.add(globalConfig); + } + } + } + + return globalConfigs; + } + + private void handle(APIGetVmInstanceMetadataFromPrimaryStorageMsg msg) { + APIGetVmInstanceMetadataFromPrimaryStorageReply reply = new APIGetVmInstanceMetadataFromPrimaryStorageReply(); + + GetVmInstanceMetadataFromPrimaryStorageMsg gmsg = new GetVmInstanceMetadataFromPrimaryStorageMsg(); + gmsg.setPrimaryStorageUuid(msg.getPrimaryStorageUuid()); + bus.makeTargetServiceIdByResourceUuid(gmsg, PrimaryStorageConstant.SERVICE_ID, msg.getPrimaryStorageUuid()); + + bus.send(gmsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + reply.setError(r.getError()); + bus.reply(msg, reply); + return; + } + GetVmInstanceMetadataFromPrimaryStorageReply re = r.castReply(); + reply.setVmInstanceMetadata(re.getVmInstanceMetadata()); + bus.reply(msg, reply); + } + }); + } } From c2328706eb911830c6a95350657a087857971be4 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:49:12 +0800 Subject: [PATCH 06/10] [vm-metadata]: registration field processor - VmInstanceMetadataFieldProcessor: field-level processing for VM metadata registration (field mapping matrix, validation, transform) Resolves: ZSV-10000 Part: 03 --- .../vm/VmInstanceMetadataFieldProcessor.java | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmInstanceMetadataFieldProcessor.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceMetadataFieldProcessor.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceMetadataFieldProcessor.java new file mode 100644 index 00000000000..e2b74d42f02 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceMetadataFieldProcessor.java @@ -0,0 +1,233 @@ +package org.zstack.compute.vm; + +import org.zstack.header.vm.VmInstanceMetadataDTO; +import org.zstack.header.vm.VmInstanceMetadataRegistrationSpec; +import org.zstack.utils.gson.JSONObjectUtil; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * 虚拟机元数据注册时的字段处理器。 + * + *

根据"注册字段处理矩阵"的规则,对反序列化后的 VO JSON 字段执行: + * 保留 / 替换 / 设 null / 重新生成 / 硬编码 等操作。

+ * + *

处理采用 Map 操作方式(而非反序列化为具体 VO 类), + * 避免字段类型变更导致的兼容性问题。

+ * + * @see VmInstanceMetadataRegistrationSpec + */ +public class VmInstanceMetadataFieldProcessor { + + private VmInstanceMetadataFieldProcessor() { + } + + // ================================================================ + // VmInstanceVO + // ================================================================ + + /** + * VmInstanceVO 中注册时需要设为 null 的字段。 + */ + private static final Set VM_NULL_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "clusterUuid", + "hostUuid", + "lastHostUuid", + "instanceOfferingUuid", + "defaultL3NetworkUuid", + "managementNetworkUuid" + ))); + + /** + * 处理 VmInstanceVO JSON。 + * + *

处理规则: + *

    + *
  • uuid/name/description/cpuNum/memorySize/platform/architecture/hypervisorType/imageUuid → 保留
  • + *
  • zoneUuid → 替换为 spec 中的新值
  • + *
  • clusterUuid/hostUuid/lastHostUuid/instanceOfferingUuid/defaultL3NetworkUuid/managementNetworkUuid → 设 null
  • + *
  • state → 硬编码为 Stopped
  • + *
  • accountUuid → 替换为 spec 中的调用者
  • + *
+ * + * @param vmVoJson 原始 VmInstanceVO JSON + * @param spec 注册参数 + * @return 处理后的 VmInstanceVO JSON + */ + @SuppressWarnings("unchecked") + public static String processVmInstanceVO(String vmVoJson, VmInstanceMetadataRegistrationSpec spec) { + Map voMap = JSONObjectUtil.toObject(vmVoJson, LinkedHashMap.class); + + for (String field : VM_NULL_FIELDS) { + voMap.put(field, null); + } + + voMap.put("zoneUuid", spec.getZoneUuid()); + voMap.put("accountUuid", spec.getAccountUuid()); + voMap.put("state", "Stopped"); + + return JSONObjectUtil.toJsonString(voMap); + } + + // ================================================================ + // VolumeVO + // ================================================================ + + /** + * VolumeVO 中注册时需要设为 null 的字段。 + */ + private static final Set VOLUME_NULL_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "diskOfferingUuid" + ))); + + /** + * 处理 VolumeVO JSON。 + * + *

处理规则: + *

    + *
  • uuid/vmInstanceUuid/name/size/type/format → 保留
  • + *
  • primaryStorageUuid → 替换为 spec 中的新主存储 UUID
  • + *
  • installPath → 路径标识符替换
  • + *
  • diskOfferingUuid → 设 null
  • + *
  • accountUuid → 替换为 spec 中的调用者
  • + *
+ * + * @param volumeVoJson 原始 VolumeVO JSON + * @param spec 注册参数 + * @return 处理后的 VolumeVO JSON + */ + @SuppressWarnings("unchecked") + public static String processVolumeVO(String volumeVoJson, VmInstanceMetadataRegistrationSpec spec) { + Map voMap = JSONObjectUtil.toObject(volumeVoJson, LinkedHashMap.class); + + for (String field : VOLUME_NULL_FIELDS) { + voMap.put(field, null); + } + + voMap.put("primaryStorageUuid", spec.getPrimaryStorageUuid()); + voMap.put("accountUuid", spec.getAccountUuid()); + + replaceInstallPath(voMap, "installPath", spec); + + return JSONObjectUtil.toJsonString(voMap); + } + + // ================================================================ + // VolumeSnapshotVO + // ================================================================ + + /** + * 处理 VolumeSnapshotVO JSON。 + * + *

处理规则: + *

    + *
  • uuid/volumeUuid/parentUuid/treeUuid/latest → 保留
  • + *
  • primaryStorageUuid → 替换为 spec 中的新主存储 UUID
  • + *
  • primaryStorageInstallPath → 路径标识符替换
  • + *
+ * + * @param snapshotVoJson 原始 VolumeSnapshotVO JSON + * @param spec 注册参数 + * @return 处理后的 VolumeSnapshotVO JSON + */ + @SuppressWarnings("unchecked") + public static String processVolumeSnapshotVO(String snapshotVoJson, VmInstanceMetadataRegistrationSpec spec) { + Map voMap = JSONObjectUtil.toObject(snapshotVoJson, LinkedHashMap.class); + + voMap.put("primaryStorageUuid", spec.getPrimaryStorageUuid()); + + replaceInstallPath(voMap, "primaryStorageInstallPath", spec); + + return JSONObjectUtil.toJsonString(voMap); + } + + // ================================================================ + // SystemTagVO / ResourceConfigVO + // ================================================================ + + /** + * 处理 SystemTagVO JSON:为 uuid 生成新值,移除自增 id。 + * + * @param tagJson 原始 SystemTagVO JSON + * @param uuidSupplier UUID 生成器(通常为 Platform::getUuid) + * @return 处理后的 SystemTagVO JSON + */ + @SuppressWarnings("unchecked") + public static String processSystemTagVO(String tagJson, Supplier uuidSupplier) { + Map tagMap = JSONObjectUtil.toObject(tagJson, LinkedHashMap.class); + + tagMap.put("uuid", uuidSupplier.get()); + tagMap.remove("id"); + + return JSONObjectUtil.toJsonString(tagMap); + } + + /** + * 处理 ResourceConfigVO JSON:为 uuid 生成新值,移除自增 id。 + * + * @param configJson 原始 ResourceConfigVO JSON + * @param uuidSupplier UUID 生成器(通常为 Platform::getUuid) + * @return 处理后的 ResourceConfigVO JSON + */ + @SuppressWarnings("unchecked") + public static String processResourceConfigVO(String configJson, Supplier uuidSupplier) { + Map configMap = JSONObjectUtil.toObject(configJson, LinkedHashMap.class); + + configMap.put("uuid", uuidSupplier.get()); + configMap.remove("id"); + + return JSONObjectUtil.toJsonString(configMap); + } + + // ================================================================ + // 跨存储过滤 + // ================================================================ + + /** + * 判断 volume 的 installPath 是否属于指定主存储。 + * + * @param volumeVoJson VolumeVO JSON + * @param pathIdentifier 存储路径标识符(如 vg uuid 或挂载路径前缀) + * @return true 表示属于该主存储 + */ + @SuppressWarnings("unchecked") + public static boolean belongsToPrimaryStorage(String volumeVoJson, String pathIdentifier) { + Map voMap = JSONObjectUtil.toObject(volumeVoJson, LinkedHashMap.class); + String installPath = (String) voMap.get("installPath"); + return installPath != null && installPath.contains(pathIdentifier); + } + + /** + * 过滤出属于指定主存储的 volume UUID 集合。 + * + *

注册时,仅处理属于当前存储的 volume 及其关联快照。 + * 不属于当前存储的 volume 跳过。

+ * + * @param dto 完整元数据 DTO + * @param pathIdentifier 旧存储路径标识符 + * @return 属于该存储的 volume resourceUuid 集合 + */ + public static Set filterVolumesByStorage(VmInstanceMetadataDTO dto, String pathIdentifier) { + if (dto.volumes == null) { + return Collections.emptySet(); + } + return dto.volumes.stream() + .filter(rm -> belongsToPrimaryStorage(rm.vo, pathIdentifier)) + .map(rm -> rm.resourceUuid) + .collect(Collectors.toSet()); + } + + // ================================================================ + // 内部工具 + // ================================================================ + + private static void replaceInstallPath(Map voMap, String fieldName, + VmInstanceMetadataRegistrationSpec spec) { + String path = (String) voMap.get(fieldName); + if (path != null && spec.getOldPathIdentifier() != null && spec.getNewPathIdentifier() != null) { + voMap.put(fieldName, path.replace(spec.getOldPathIdentifier(), spec.getNewPathIdentifier())); + } + } +} \ No newline at end of file From fbaa6b8689091e4b53b8cb06ce58822e5f488bb0 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:49:22 +0800 Subject: [PATCH 07/10] [vm-metadata]: test infrastructure updates - ApiHelper: metadata API test helpers (scan, read, register, etc.) - KVMSimulator: metadata agent command simulators Resolves: ZSV-10000 Part: 07 --- .../java/org/zstack/testlib/ApiHelper.groovy | 27 +++++++++++++++++++ .../org/zstack/testlib/KVMSimulator.groovy | 10 +++++++ 2 files changed, 37 insertions(+) diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 07c05b73b9e..5ddb04eac93 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -27406,6 +27406,33 @@ abstract class ApiHelper { } + def registerVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RegisterVmInstanceAction.class) Closure c) { + def a = new org.zstack.sdk.RegisterVmInstanceAction() + 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 diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 94fc178245d..fcfb5a8ff78 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -680,5 +680,15 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_UPDATE_HOSTNAME_PATH) { return new UpdateHostnameRsp() } + + spec.simulator(KVMConstant.WRITE_VM_INSTANCE_METADATA_PATH) { HttpEntity e -> + return new WriteVmInstanceMetadataRsp() + } + + spec.simulator(KVMConstant.READ_VM_INSTANCE_METADATA_PATH) { HttpEntity e -> + def rsp = new ReadVmInstanceMetadataRsp() + rsp.metadata = "{\"vmInstanceVO\":\"{\\\"vmNics\\\":[{\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"l3NetworkUuid\\\":\\\"28d3a9c8e54c48f290ab4f9e52bbb006\\\",\\\"mac\\\":\\\"fa:81:16:b2:32:00\\\",\\\"hypervisorType\\\":\\\"KVM\\\",\\\"deviceId\\\":0,\\\"internalName\\\":\\\"vnic1.0\\\",\\\"driverType\\\":\\\"virtio\\\",\\\"type\\\":\\\"VNIC\\\",\\\"state\\\":\\\"enable\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"usedIps\\\":[],\\\"uuid\\\":\\\"a77234a5a45a4a7caca46d01d746f41f\\\",\\\"resourceType\\\":\\\"VmNicVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.vm.VmNicVO\\\"}],\\\"allVolumes\\\":[{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":2,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":2,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"},{\\\"name\\\":\\\"ROOT-for-vmName\\\",\\\"description\\\":\\\"Root volume for VM[uuid:77bc3074f5f4438c836ce6c56bc5a4aa]\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"rootImageUuid\\\":\\\"575591e021b446e4b465e981da3a8d1b\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"type\\\":\\\"Root\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":0,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"ROOT-for-vmName\\\",\\\"description\\\":\\\"Root volume for VM[uuid:77bc3074f5f4438c836ce6c56bc5a4aa]\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"rootImageUuid\\\":\\\"575591e021b446e4b465e981da3a8d1b\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"type\\\":\\\"Root\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":0,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceName\\\":\\\"ROOT-for-vmName\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"},{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/43436624dc714282913e0a141246629e\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":1,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/43436624dc714282913e0a141246629e\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":1,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"},{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":3,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":3,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"}],\\\"vmCdRoms\\\":[{\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"deviceId\\\":0,\\\"name\\\":\\\"vm-77bc3074f5f4438c836ce6c56bc5a4aa-cdRom\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"uuid\\\":\\\"e8a57f5b8c834573b4da822b672740e4\\\",\\\"resourceName\\\":\\\"vm-77bc3074f5f4438c836ce6c56bc5a4aa-cdRom\\\",\\\"resourceType\\\":\\\"VmCdRomVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.vm.cdrom.VmCdRomVO\\\"}],\\\"name\\\":\\\"vmName\\\",\\\"zoneUuid\\\":\\\"d71de3f6981d46c9a2be43e5fcf31021\\\",\\\"clusterUuid\\\":\\\"29f13acb820d4f7f8cd3593b79b742e5\\\",\\\"imageUuid\\\":\\\"575591e021b446e4b465e981da3a8d1b\\\",\\\"hostUuid\\\":\\\"e99debc09c5845fb8ed682320117f4ce\\\",\\\"internalId\\\":1,\\\"lastHostUuid\\\":\\\"e99debc09c5845fb8ed682320117f4ce\\\",\\\"rootVolumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"defaultL3NetworkUuid\\\":\\\"28d3a9c8e54c48f290ab4f9e52bbb006\\\",\\\"type\\\":\\\"UserVm\\\",\\\"hypervisorType\\\":\\\"KVM\\\",\\\"cpuNum\\\":1,\\\"cpuSpeed\\\":0,\\\"memorySize\\\":1073741824,\\\"reservedMemorySize\\\":0,\\\"platform\\\":\\\"Linux\\\",\\\"architecture\\\":\\\"x86_64\\\",\\\"guestOsType\\\":\\\"CentOS\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:45 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"state\\\":\\\"Running\\\",\\\"uuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceName\\\":\\\"vmName\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.vm.VmInstanceVO\\\"}\",\"vmSystemTags\":[\"{\\\"inherent\\\":false,\\\"uuid\\\":\\\"38a9b4bd1b8b3dfa829d582aafb2ec25\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"tag\\\":\\\"syncPorts::77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:45 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:45 AM\\\"}\",\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"3e984cdb5edb47559a3f907e1d49bfcc\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"tag\\\":\\\"additionalQmp\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\",\"{\\\"inherent\\\":false,\\\"uuid\\\":\\\"85237d3a06133523bd84669349040ec5\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"tag\\\":\\\"vmPriority::Normal\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\",\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"b7c5d5e94ba13159ab2c8c65c1d7bc29\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"tag\\\":\\\"vmSystemSerialNumber::8ed14f00-50bb-4e9e-9448-e92c0f67e1e1\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\",\"{\\\"inherent\\\":false,\\\"uuid\\\":\\\"d5019730aeba3e57b2f1a3e8d74d0cbc\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"tag\\\":\\\"ha::None\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\"],\"vmResourceConfigs\":[\"{\\\"uuid\\\":\\\"8d2f9937a28846aba03fded826c10c73\\\",\\\"resourceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"resourceType\\\":\\\"VmInstanceVO\\\",\\\"name\\\":\\\"nicMultiQueueNum\\\",\\\"description\\\":\\\"default num of queues on virtio nic\\\",\\\"category\\\":\\\"vm\\\",\\\"value\\\":\\\"1\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\"],\"volumeVOs\":[\"{\\\"name\\\":\\\"ROOT-for-vmName\\\",\\\"description\\\":\\\"Root volume for VM[uuid:77bc3074f5f4438c836ce6c56bc5a4aa]\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"rootImageUuid\\\":\\\"575591e021b446e4b465e981da3a8d1b\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"type\\\":\\\"Root\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":0,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"ROOT-for-vmName\\\",\\\"description\\\":\\\"Root volume for VM[uuid:77bc3074f5f4438c836ce6c56bc5a4aa]\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"rootImageUuid\\\":\\\"575591e021b446e4b465e981da3a8d1b\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"type\\\":\\\"Root\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":0,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceName\\\":\\\"ROOT-for-vmName\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"}\",\"{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":3,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":3,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"}\",\"{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":2,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":2,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"}\",\"{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/43436624dc714282913e0a141246629e\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":1,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"isShareable\\\":false,\\\"shadow\\\":{\\\"name\\\":\\\"volumeName1null\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"installPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/43436624dc714282913e0a141246629e\\\",\\\"type\\\":\\\"Data\\\",\\\"status\\\":\\\"Ready\\\",\\\"size\\\":1073741824,\\\"actualSize\\\":0,\\\"deviceId\\\":1,\\\"format\\\":\\\"qcow2\\\",\\\"state\\\":\\\"Enabled\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"isShareable\\\":false},\\\"uuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"resourceName\\\":\\\"volumeName1null\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.volume.VolumeVO\\\"}\"],\"volumeSystemTags\":{\"b7290c15276b4700af2c1b108b2b62e1\":[\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"b9874ec02b583538a5603e7eec8c5b69\\\",\\\"resourceUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"tag\\\":\\\"kvm::volume::0x000f59f934d14a68\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\"],\"8d1e76eca52647f5a4544b9ff2d370de\":[\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"96cb4b006708387b8318f0fd6ae6ab8b\\\",\\\"resourceUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"tag\\\":\\\"kvm::volume::0x000faad0c9ca4231\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:48 AM\\\"}\"],\"ae9f28cb5055498e8661793d204208ba\":[\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"5ceacd06bf753b0c8abe5bcef9b5a894\\\",\\\"resourceUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"tag\\\":\\\"kvm::volume::0x000fc4ffeaab6e71\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\"],\"db8251e870b14d60ace863a7598cce8b\":[\"{\\\"inherent\\\":true,\\\"uuid\\\":\\\"d53865baa675373a9bf07a6f501eab41\\\",\\\"resourceUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"resourceType\\\":\\\"VolumeVO\\\",\\\"tag\\\":\\\"kvm::volume::0x000fad154165d205\\\",\\\"type\\\":\\\"System\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}\"]},\"volumeResourceConfigs\":{\"b7290c15276b4700af2c1b108b2b62e1\":[],\"8d1e76eca52647f5a4544b9ff2d370de\":[],\"ae9f28cb5055498e8661793d204208ba\":[],\"db8251e870b14d60ace863a7598cce8b\":[]},\"vmNicVOs\":[\"{\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"l3NetworkUuid\\\":\\\"28d3a9c8e54c48f290ab4f9e52bbb006\\\",\\\"mac\\\":\\\"fa:81:16:b2:32:00\\\",\\\"hypervisorType\\\":\\\"KVM\\\",\\\"deviceId\\\":0,\\\"internalName\\\":\\\"vnic1.0\\\",\\\"driverType\\\":\\\"virtio\\\",\\\"type\\\":\\\"VNIC\\\",\\\"state\\\":\\\"enable\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:46 AM\\\",\\\"usedIps\\\":[],\\\"uuid\\\":\\\"a77234a5a45a4a7caca46d01d746f41f\\\",\\\"resourceType\\\":\\\"VmNicVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.vm.VmNicVO\\\"}\"],\"vmNicSystemTags\":{\"a77234a5a45a4a7caca46d01d746f41f\":[]},\"vmNicResourceConfigs\":{\"a77234a5a45a4a7caca46d01d746f41f\":[]},\"volumeSnapshots\":{\"b7290c15276b4700af2c1b108b2b62e1\":[\"{\\\"uuid\\\":\\\"7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"treeUuid\\\":\\\"f8042fb57bb04ebcb0f01bab2abeb5dd\\\",\\\"parentUuid\\\":\\\"a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":true,\\\"size\\\":1,\\\"distance\\\":4,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\"}\",\"{\\\"uuid\\\":\\\"a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"treeUuid\\\":\\\"f8042fb57bb04ebcb0f01bab2abeb5dd\\\",\\\"parentUuid\\\":\\\"b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":3,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\"}\",\"{\\\"uuid\\\":\\\"b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"treeUuid\\\":\\\"f8042fb57bb04ebcb0f01bab2abeb5dd\\\",\\\"parentUuid\\\":\\\"bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":2,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\"}\",\"{\\\"uuid\\\":\\\"bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"treeUuid\\\":\\\"f8042fb57bb04ebcb0f01bab2abeb5dd\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":1,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:51 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\"}\"],\"8d1e76eca52647f5a4544b9ff2d370de\":[\"{\\\"uuid\\\":\\\"04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"treeUuid\\\":\\\"d4a030087ed3407894c393ee81f0bc3b\\\",\\\"parentUuid\\\":\\\"79caace79a1048d58ea7c0b38815bbd0\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/79caace79a1048d58ea7c0b38815bbd0\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":0,\\\"distance\\\":3,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\"}\",\"{\\\"uuid\\\":\\\"61e2ada0170142bb8b303910a27690aa\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"treeUuid\\\":\\\"d4a030087ed3407894c393ee81f0bc3b\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":0,\\\"distance\\\":1,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:51 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\"}\",\"{\\\"uuid\\\":\\\"79caace79a1048d58ea7c0b38815bbd0\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"treeUuid\\\":\\\"d4a030087ed3407894c393ee81f0bc3b\\\",\\\"parentUuid\\\":\\\"61e2ada0170142bb8b303910a27690aa\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/61e2ada0170142bb8b303910a27690aa\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":0,\\\"distance\\\":2,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\"}\",\"{\\\"uuid\\\":\\\"bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"treeUuid\\\":\\\"d4a030087ed3407894c393ee81f0bc3b\\\",\\\"parentUuid\\\":\\\"04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":true,\\\"size\\\":0,\\\"distance\\\":4,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\"}\"],\"ae9f28cb5055498e8661793d204208ba\":[\"{\\\"uuid\\\":\\\"1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"treeUuid\\\":\\\"055b80b0727e4117b246b1b29f2d58b6\\\",\\\"parentUuid\\\":\\\"aefbe47465c047d1b118321c34425869\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/aefbe47465c047d1b118321c34425869\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":3,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\"}\",\"{\\\"uuid\\\":\\\"69e85ac72fea4263a55cbcd21785006e\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"treeUuid\\\":\\\"055b80b0727e4117b246b1b29f2d58b6\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":1,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:51 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\"}\",\"{\\\"uuid\\\":\\\"aefbe47465c047d1b118321c34425869\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"treeUuid\\\":\\\"055b80b0727e4117b246b1b29f2d58b6\\\",\\\"parentUuid\\\":\\\"69e85ac72fea4263a55cbcd21785006e\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/69e85ac72fea4263a55cbcd21785006e\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":2,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\"}\",\"{\\\"uuid\\\":\\\"b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"treeUuid\\\":\\\"055b80b0727e4117b246b1b29f2d58b6\\\",\\\"parentUuid\\\":\\\"1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":true,\\\"size\\\":1,\\\"distance\\\":4,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\"}\"],\"db8251e870b14d60ace863a7598cce8b\":[\"{\\\"uuid\\\":\\\"43436624dc714282913e0a141246629e\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"treeUuid\\\":\\\"1c0773fa98f4465b8e535ba3c00dc039\\\",\\\"parentUuid\\\":\\\"a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":true,\\\"size\\\":1,\\\"distance\\\":4,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\"}\",\"{\\\"uuid\\\":\\\"791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"treeUuid\\\":\\\"1c0773fa98f4465b8e535ba3c00dc039\\\",\\\"parentUuid\\\":\\\"a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":2,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\"}\",\"{\\\"uuid\\\":\\\"a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"treeUuid\\\":\\\"1c0773fa98f4465b8e535ba3c00dc039\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":1,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:51 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:53 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\"}\",\"{\\\"uuid\\\":\\\"a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"type\\\":\\\"Hypervisor\\\",\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"treeUuid\\\":\\\"1c0773fa98f4465b8e535ba3c00dc039\\\",\\\"parentUuid\\\":\\\"791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"primaryStorageUuid\\\":\\\"e121a11157bb4746ad3c8d56c3760a3e\\\",\\\"primaryStorageInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"format\\\":\\\"qcow2\\\",\\\"latest\\\":false,\\\"size\\\":1,\\\"distance\\\":3,\\\"state\\\":\\\"Enabled\\\",\\\"status\\\":\\\"Ready\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"backupStorageRefs\\\":[],\\\"groupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\"}\"]},\"volumeSnapshotGroupVO\":[\"{\\\"snapshotCount\\\":4,\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeSnapshotRefs\\\":[{\\\"volumeSnapshotUuid\\\":\\\"a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"69e85ac72fea4263a55cbcd21785006e\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"61e2ada0170142bb8b303910a27690aa\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}],\\\"uuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"resourceName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceType\\\":\\\"VolumeSnapshotGroupVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO\\\"}\",\"{\\\"snapshotCount\\\":4,\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeSnapshotRefs\\\":[{\\\"volumeSnapshotUuid\\\":\\\"b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"79caace79a1048d58ea7c0b38815bbd0\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/61e2ada0170142bb8b303910a27690aa\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"aefbe47465c047d1b118321c34425869\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/69e85ac72fea4263a55cbcd21785006e\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}],\\\"uuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"resourceName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceType\\\":\\\"VolumeSnapshotGroupVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO\\\"}\",\"{\\\"snapshotCount\\\":4,\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeSnapshotRefs\\\":[{\\\"volumeSnapshotUuid\\\":\\\"1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/aefbe47465c047d1b118321c34425869\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/79caace79a1048d58ea7c0b38815bbd0\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}],\\\"uuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"resourceName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceType\\\":\\\"VolumeSnapshotGroupVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO\\\"}\",\"{\\\"snapshotCount\\\":4,\\\"name\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"vmInstanceUuid\\\":\\\"77bc3074f5f4438c836ce6c56bc5a4aa\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeSnapshotRefs\\\":[{\\\"volumeSnapshotUuid\\\":\\\"43436624dc714282913e0a141246629e\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"},{\\\"volumeSnapshotUuid\\\":\\\"7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}],\\\"uuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"resourceName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"resourceType\\\":\\\"VolumeSnapshotGroupVO\\\",\\\"concreteResourceType\\\":\\\"org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO\\\"}\"],\"volumeSnapshotGroupRefVO\":[\"{\\\"volumeSnapshotUuid\\\":\\\"04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/79caace79a1048d58ea7c0b38815bbd0\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/aefbe47465c047d1b118321c34425869\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"43436624dc714282913e0a141246629e\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"61e2ada0170142bb8b303910a27690aa\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"69e85ac72fea4263a55cbcd21785006e\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"7832daf63d9b41d68bd1460c20ed0e0a\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"79caace79a1048d58ea7c0b38815bbd0\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/61e2ada0170142bb8b303910a27690aa\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"a31c3de68ce246538e982e0e5c7d2d73\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"a35b7ae1616a4974b2f80654c5527fbb\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"a70bb2be871644b6ad12ac8d6e9524d0\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"7648a93930db473785b0abc0e0716c1a\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":1,\\\"volumeUuid\\\":\\\"db8251e870b14d60ace863a7598cce8b\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/791f523bc99a4f08bd70b5a59d8ed5c8\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:56 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:49 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"aefbe47465c047d1b118321c34425869\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/69e85ac72fea4263a55cbcd21785006e\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"b5d771aa83584c9c88d9b84147dfc9ad\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"6db066c890d141008e8ff18bd5940d77\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:54 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"b711f22ad5c045b6ad1d770d4f301d05\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":3,\\\"volumeUuid\\\":\\\"ae9f28cb5055498e8661793d204208ba\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/1aaa92b2d1eb4c36bb8951b8e1521b34\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"bc5ab54cb3d04635923a2a9d0b5fc73f\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"d2d486455d6c472cbb7391958edebea5\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":0,\\\"volumeUuid\\\":\\\"8d1e76eca52647f5a4544b9ff2d370de\\\",\\\"volumeName\\\":\\\"ROOT-for-vmName\\\",\\\"volumeType\\\":\\\"Root\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/04ef6d31675f4ba5816b104920dc3e2c\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-ROOT-for-vmName\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:57 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:47 AM\\\"}\",\"{\\\"volumeSnapshotUuid\\\":\\\"bcc3ed8070984cd691c62f421aeaa44d\\\",\\\"volumeSnapshotGroupUuid\\\":\\\"01171364e6704dbe83c36167de52d719\\\",\\\"snapshotDeleted\\\":false,\\\"deviceId\\\":2,\\\"volumeUuid\\\":\\\"b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeName\\\":\\\"volumeName1null\\\",\\\"volumeType\\\":\\\"Data\\\",\\\"volumeSnapshotInstallPath\\\":\\\"sharedblock://e121a11157bb4746ad3c8d56c3760a3e/b7290c15276b4700af2c1b108b2b62e1\\\",\\\"volumeSnapshotName\\\":\\\"group-8d1e76eca52647f5a4544b9ff2d370de-volumeName1null\\\",\\\"createDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"lastOpDate\\\":\\\"Jan 28, 2026 4:15:52 AM\\\",\\\"volumeLastAttachDate\\\":\\\"Jan 28, 2026 4:15:50 AM\\\"}\"],\"volumeSnapshotReferenceVO\":{},\"volumeSnapshotReferenceTreeVO\":{},\"EncryptedResourceKeyRefVO\":{}}" + return rsp + } } } From 4094183f296232700f2dfdb94125022926863a83 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:49:35 +0800 Subject: [PATCH 08/10] [vm-metadata]: design documents - Part 00: Design constraint index - Part 01a: Data model and serialization - Part 01b: API interception and VM resolution - Part 01c: Storage layer and template VM - Part 02: Dirty mark and Poller - Part 02b: HA and operations - Part 03: Registration and operations - Part 04a-04e: SharedBlock storage protocol - Part 05: API design - Part 07a-07d: Test plans (unit/integration/fault/performance) Resolves: ZSV-10000 --- ...46\346\235\237\347\264\242\345\274\225.md" | 94 ++ ...16\345\272\217\345\210\227\345\214\226.md" | 417 ++++++++ ...\344\270\216VM\350\247\243\346\236\220.md" | 433 ++++++++ ...77\350\231\232\346\213\237\346\234\272.md" | 464 +++++++++ ...\240\207\350\256\260\344\270\216Poller.md" | 939 +++++++++++++++++ ...50\344\270\216\350\277\220\347\273\264.md" | 942 ++++++++++++++++++ ...14\344\270\216\350\277\220\347\273\264.md" | 572 +++++++++++ ...17\350\256\256\346\246\202\350\277\260.md" | 248 +++++ ...33\345\210\266\345\270\203\345\261\200.md" | 289 ++++++ ...31\345\205\245\346\265\201\347\250\213.md" | 268 +++++ ...26\344\270\216\346\201\242\345\244\215.md" | 441 ++++++++ ...\350\277\220\347\273\264\344\270\216IO.md" | 432 ++++++++ ...etadata-05-API\350\256\276\350\256\241.md" | 521 ++++++++++ ...13\350\257\225\350\256\241\345\210\222.md" | 278 ++++++ ...13\350\257\225\350\256\241\345\210\222.md" | 276 +++++ ...50\345\205\245\346\265\213\350\257\225.md" | 183 ++++ ...45\345\205\205\346\265\213\350\257\225.md" | 261 +++++ 17 files changed, 7058 insertions(+) create mode 100644 "docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" create mode 100644 "docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" create mode 100644 "docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" create mode 100644 "docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" create mode 100644 "docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" create mode 100644 "docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" create mode 100644 "docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" create mode 100644 "docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" create mode 100644 "docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" create mode 100644 "docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" create mode 100644 "docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" create mode 100644 "docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" create mode 100644 "docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" create mode 100644 "docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" create mode 100644 "docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" create mode 100644 "docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" create mode 100644 "docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" diff --git "a/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" "b/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" new file mode 100644 index 00000000000..1a343a0dd96 --- /dev/null +++ "b/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" @@ -0,0 +1,94 @@ +# VM 元数据 — 设计约束索引 + +> 本文档汇总所有 vm-metadata 设计文档中的约束条目(C-*),提供跨文档查找的统一入口。 +> 每条约束标注 ID、简要描述和来源文档/章节。 + +## Part 1b — 拦截器(vm-metadata-01b-API拦截与VM解析.md) + +| ID | 约束 | 来源 | +|----|------|------| +| C-IC | `INTERNAL_METADATA_MESSAGES` 与 handler `markDirty()` 调用点一一可追溯 | §5 | +| C-IM | 所有 `APIMessage` 子类必须标注 `@MetadataImpact`(可为 NONE),CI 扫描全量子类 | §5 | +| C-PA | `pendingApis` 超时清理 + afterCompletion null-safe + 清理时补 markDirty | §5 | +| C-RS | Resolver 选择匹配 API 资源语义;删除/卸载类 API 使用 pre-capture | §5 | +| C-H1 | STORAGE 级内部消息 handler 必须调用 `markDirty()`;CI ERROR 阻断 | §5 | +| C-M4 | `pendingApis` 超时通过 GlobalConfig 配置,不得硬编码 | §5 | + +## Part 1c — 存储层(vm-metadata-01c-存储层与模板虚拟机.md) + +| ID | 约束 | 来源 | +|----|------|------| +| C-01C-2 | sblk LV 名称 `{vm_uuid}_vmmeta`,长度 39,< LVM 128 上限 | §4 | +| C-01C-3 | 模板 VM 元数据锚定 RootVolume 所在 PS | §4 | +| C-01C-4 | 存储迁移:目标端同步写入 + read-back 校验后才能清理源端 | §4 | +| C-01C-5 | 清理时校验根盘 `primaryStorageUuid` 仍在源 PS | §4 | +| C-01C-6 | flush 路径动态解析,不缓存历史路径 | §4 | +| C-01C-7 | 迁移 `nextRetryTime` 暂停/恢复成对;失败回滚恢复 Poller | §4 | +| C-01C-8 | MN 启动时重置 `nextRetryTime='2099-...'` 的暂停行(Poller 启动前) | §4 | +| C-01C-9 | `deleteMetadata` 幂等(不存在不抛异常) | §4 | +| C-01C-10 | local/NFS tmp 文件固定命名,Agent 启动时清理残留 | §4 | +| C-01C-11 | `MetadataStorageHandler` 包含 `scanMetadataVmUuids()`(Q15) | §4 | +| C-01C-12 | `deleteMetadata` 重试参数通过 GlobalConfig 配置(Q12) | §4 | + +## Part 2 — Dirty Mark & Poller(vm-metadata-02-脏标记与Poller.md) + +| ID | 约束 | 来源 | +|----|------|------| +| C-DM-01 | `markDirty` 使用 `INSERT IGNORE + UPDATE` 两步,禁止 `ON DUPLICATE KEY`(Q19) | §7 | +| C-CL-02 | claim 成功必须写入 `lastClaimTime`;僵尸清理 15 分钟(独立任务 DP-05) | §7 | +| C-TM-03 | `doFlush` 超时 ≥ 5 分钟,超时进入 `onFlushFailure` | §7 | +| C-RB-04 | 指数退避参数来自 GlobalConfig(baseDelay/maxExponent) | §7 | +| C-SR-05 | 重试耗尽必须标记 `lastFlushFailed=true`,不得静默放弃 | §7 | +| C-SR-06 | StaleRecoveryTask 的 markDirty 使用 retryCount=0,验证返回值后才清除 lastFlushFailed(DP-03) | §7 | +| C-SC-07 | `storageStructureChange` 仅在存储拓扑操作时设置;升级场景始终 true | §7 | +| C-FL-08 | `doFlush` 过滤 Destroyed VM dirty 行,主动删除释放 Poller(Q34) | §7 | +| C-TF-09 | `triggerFlushForVm` stale claim 接管阈值通过 `vm.metadata.triggerFlush.staleMinutes` 配置(默认 10 min),不得与 `staleClaim.thresholdMinutes`(30 min 后台扫描)混淆 | §7, DP-06 | + +## Part 2b — HA & 运维(vm-metadata-02b-高可用与运维.md) + +| ID | 约束 | 来源 | +|----|------|------| +| C-02B-1 | `nodeLeft()` 延迟 5s 后触发 `claimAndFlush()`,不立即抢占 | §15 | +| C-02B-2 | sblk 写入前校验 `managementNodeUuid == 本 MN`(Fence Check) | §15 | +| C-02B-3 | 路径巡检禁止 `listAll`,必须 keyset 分页 | §15 | +| C-02B-4 | 升级刷新分批执行(默认 1000),避免单次超大事务 | §15 | +| C-02B-5 | payload 上限:静态 30MB + 运行时 slot 容量校验 | §15 | +| C-02B-6 | `storageStructureChange` OR 语义,dirty 行删除前不降级 | §15 | +| C-02B-7 | 容量常量集中定义,禁止硬编码散落 | §15 | +| C-02B-8 | `lastFlushFailed` 仅 retry 耗尽时 true,仅 StaleRecoveryTask 重置 false | §15 | +| C-02B-9 | 升级刷新前检查 15 分钟内无 nodeLeft(M3) | §15 | +| C-02B-10 | nodeLeft 延迟调整需与 Fence Check 配合评估 | §15 | +| C-02B-11 | `false→true` 初始化分批 + 批间延迟 | §15 | +| C-02B-12 | Cleanup API 仅 enabled=false 时允许 | §15 | +| C-02B-13 | 初始化每批重检 enabled 开关 | §15 | +| C-02B-14 | 孤儿检测仅报告不自动删除 | §15 | + +## Part 3 — 注册(vm-metadata-03-注册与运维.md) + +| ID | 约束 | 来源 | +|----|------|------| +| C-03-1 | `parentId` 注册时统一置 null | §9 | +| C-03-2 | 跨存储拒绝注册,返回 expected/actual PS UUID | §9 | +| C-03-3 | installPath 前缀替换满足分隔符边界 | §9 | +| C-03-4 | 回滚"由外到内"+ 空树清理 SQL | §9 | +| C-03-5 | ChainTask 超时 35 分钟;LongJob cancel 触发 rollback | §9 | +| C-03-6 | Root Volume path 缺失 BLOCK;Data Volume WARN | §9 | +| C-03-7 | 注册成功后触发 ConsistencyCheck | §9 | +| C-03-8 | PreCheck 与 Register 共享校验方法 | §9 | + +--- + +**总计**:6(Part 1b)+ 11(Part 1c)+ 8(Part 2)+ 14(Part 2b)+ 8(Part 3)= **47 条约束** + +--- + +## Part 7 — 测试计划 + +测试计划分为 4 个文档,约 190+ 条用例,按约束 ID 交叉引用: + +| 文档 | 范围 | 用例前缀 | +|------|------|----------| +| [Part 7a — 单元测试](vm-metadata-07a-单元测试计划.md) | 序列化 Round-Trip、DTO 构建、路径指纹、markDirty 逻辑、注解覆盖率、Resolver 链、容量计算、sblk 编解码、注册字段映射、installPath 替换 | UT-* | +| [Part 7b — 集成测试](vm-metadata-07b-集成测试计划.md) | sblk 写入读取、local/NFS JSON 读写、Poller 端到端、API 拦截器联动、存储迁移链路、注册端到端、路径巡检、API 端到端 | IT-* | +| [Part 7c — 故障注入](vm-metadata-07c-故障注入测试.md) | sblk 三阶段崩溃恢复、MN 重启清理、双 MN 故障转移、DB 异常、Agent 异常、功能开关竞态 | FI-* | +| [Part 7d — 性能与补充](vm-metadata-07d-性能与补充测试.md) | 1000/10000 VM 全量更新基准、升级批次压力、注册耗时、Poller 吞吐、E2E 场景、兼容性、安全权限、可观测性、GlobalConfig 动态生效 | PERF-*/E2E-*/COMPAT-*/SEC-*/OBS-*/CFG-* | diff --git "a/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" "b/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000000..3ed69abdd49 --- /dev/null +++ "b/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,417 @@ +# VM 元数据 — 数据模型与序列化 + +## 目录 + +1. [概述](#1-概述) +2. [核心 DTO 结构](#2-核心-dto-结构) +3. [编码策略 — per-Resource 字段级 Base64](#3-编码策略--per-resource-字段级-base64) +4. [序列化关注点](#4-序列化关注点) +5. [反序列化关注点](#5-反序列化关注点) +6. [schemaVersion 版本规则](#6-schemaversion-版本规则) +7. [VmCdRomVO 等附属资源](#7-vmcdromvo-等附属资源) + +--- + +## 1. 概述 + +虚拟机元数据功能用于将 VM 及其关联资源(云盘、网卡、快照、SystemTag、ResourceConfig)的关键信息持久化到主存储上,以支持跨平台/灾难恢复场景下的虚拟机注册恢复。 + +### 1.1 适用范围 + +本功能仅适用于 **`type = "UserVm"`** 的虚拟机实例。ApplianceVm(虚拟路由器、网关等系统 VM)不写入元数据、不支持注册。 + +`@MetadataImpact` 拦截器和 `buildVmInstanceMetadata()` 中均增加 `vmInstanceType != "UserVm"` 前置检查,不满足时静默跳过。 + +### 1.2 全局配置 + +增加全局配置项 `vm.metadata.enabled`(Boolean,**默认为 false**),开启/关闭记录虚拟机元数据。 + +**理由**:注册虚拟机仅用于有容灾需求的场景。对于 99.9% 普通用户来说不会用到此功能。 + +#### 1.2.1 开关切换策略 + +| 切换方向 | 行为 | 说明 | +|----------|------|------| +| **`false → true`** | 触发一次**分批全量 markDirty** 初始化所有 VM 的元数据 | 防止读写风暴,复用 Poller 自动限流 | +| **`true → false`** | **不自动删除**已有元数据文件/LV | 提供 `APICleanupVmInstanceMetadataMsg` 按需清理 | + +**`false → true`(启用)详细流程**: + +通过 `GlobalConfig.installUpdateExtension` 监听 `vm.metadata.enabled` 变更。检测到从 `false` 变为 `true` 时,提交延迟 30 秒的初始化任务(等待 Poller 启动就绪),执行分批 markDirty: + +```java +VmGlobalConfig.VM_METADATA_ENABLED.installUpdateExtension((oldValue, newValue) -> { + boolean wasEnabled = Boolean.parseBoolean(oldValue); + boolean nowEnabled = Boolean.parseBoolean(newValue); + + if (!wasEnabled && nowEnabled) { + // false → true: 分批初始化全量 VM 元数据 + submitBatchInitialization(); + } + // true → false: 不做任何自动操作 +}); +``` + +`submitBatchInitialization()` 逻辑与升级全量刷新([Part 2b §9.2](vm-metadata-02b-高可用与运维.md#92-刷新执行简化无-longjob))共用相同的分批 SQL 模式,但使用独立配置项控制批次大小和批间延迟(详见 [Part 2b §9a](vm-metadata-02b-高可用与运维.md#9a-功能开关切换处理))。 + +**`true → false`(禁用)详细说明**: + +- Poller 的 `markDirty()` 和 `triggerFlushForVm()` 内前置检查 `vm.metadata.enabled`,关闭后自动停止新的标脏和刷写 +- 已存在的 dirty 行不主动清理(自然过期或下次启用时重新处理) +- 已写入存储的元数据文件/LV **保留不删除**,避免误操作导致已有元数据丢失 +- 运维可通过 `APICleanupVmInstanceMetadataMsg`(见 [Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据))按需批量清理指定 PS 或指定 VM 的元数据 + +### 1.3 安全声明 + +- USERDATA 和 SystemTag 中可能包含 cloud-init 脚本 或者 password 信息。 +- 未来如需加密,可通过 bump HeaderVersion 引入 `EncryptionType` 字段扩展。 + +### 1.4 命名规范 + +| 层面 | 前缀 | 示例 | +|------|------|------| +| DTO(对应 VmInstanceVO) | `VmInstanceMetadata*` | `VmInstanceMetadataDTO`、`VmInstanceMetadataCodec` | +| VO / DB 表 / 内部组件 | `VmMetadata*` | `VmMetadataDirtyVO`、`VmMetadataPathFingerprintVO`、`MetadataDirtyPoller` | + +### 1.5 构建事务性能监控 + +`buildVmInstanceMetadata()` 运行在 `REPEATABLE READ` 事务中以保障快照一致性。: + +- 处置建议:暂时不用考虑,可预期范围内,一个虚拟机没有那么多盘和快照,查询会很快。且元数据更新保证最终一致性。 + +--- + +## 2. 核心 DTO 结构 + +### 2.1 VmInstanceMetadataDTO + +``` +VmInstanceMetadataDTO +├── schemaVersion: String // 元数据 schema 版本(与 zsv 数据库版本一致) +├── vmCategory: VmMetadataCategory // VM 类型(REGULAR / TEMPLATE / TEMPLATE_CACHE) +├── vm: ResourceMetadata // 虚拟机自身 +├── volumes: List // 根盘 + 数据盘(含引用) +├── nics: List // 网卡(仅记录,注册时不恢复) +├── snapshots: List // 全部卷的 VolumeSnapshotVO JSON 列表(VM 级别扁平化) +├── snapshotGroups: List // List(VM 级别,横跨多卷) +└── snapshotGroupRefs: List // List(VM 级别) +``` + +**前置约束**: +- VM 必须有 Root Volume。无 Root Volume 的 VM(非法状态)跳过元数据构建,`markDirty()` 时以 WARN 日志记录。 +- `volumes` 列表仅包含 `isShareable=false` 的 Volume。共享盘(`isShareable=true`)不纳入元数据,注册时也不恢复。理由:共享盘可能同时挂载在多个 VM 上,跨平台恢复时无法保证共享语义的一致性。 +- **共享盘快照排除(讨论澄清)**:构建 `snapshots` 列表时,查询条件应排除共享盘的快照(`WHERE volumeUuid IN (非共享盘 UUID 列表)`)。若 VM 的数据盘中有共享盘,其快照也不纳入元数据。 +- **空快照列表防护(讨论澄清)**:若 VM 的所有卷均无快照(`allSnapshots.isEmpty()`),跳过 `VolumeSnapshotTree.fromVOs()` 调用,直接设置 `dto.snapshots = Collections.emptyList()`。避免向空输入传递导致潜在的 NPE。 + +**确定性排序规则**:DB 数据不变时,多次构建必须产出完全相同的 JSON。所有 List 字段在序列化前按主键升序排列: + +| 字段 | 排序键 | +|------|--------| +| `volumes` | `VolumeVO.uuid` | +| `nics` | `VmNicVO.uuid` | +| `snapshots` | **BFS 拓扑排序**(见下方说明) | +| `snapshotGroups` | `VolumeSnapshotGroupVO.uuid` | +| `snapshotGroupRefs` | `volumeSnapshotGroupUuid` 优先,`volumeUuid` 次之(复合键字典序) | +| `VolumeResourceMetadata.snapshotReferences` | `VolumeSnapshotReferenceVO.id` | +| `VolumeResourceMetadata.snapshotReferenceTrees` | `VolumeSnapshotReferenceTreeVO.uuid` | +| `systemTags`(Base64 编码前) | `SystemTagVO.uuid` | +| `resourceConfigs`(Base64 编码前) | `ResourceConfigVO.uuid` | + +**snapshots 拓扑排序规则**(保证父快照在子快照之前,支持注册时按序恢复): + +``` +1. 按 volumeUuid 分组,每组再按 treeUuid 分组 +2. 同一 tree 内:使用已有的 VolumeSnapshotTree.fromVOs() + levelOrderTraversal() + - 根节点(parentUuid = null)排最前 + - BFS 层序遍历,父先于子 +3. 不同 tree 之间按 treeUuid ASC 排列 +4. 不同 volume 之间按 volumeUuid ASC 排列 +``` + +保证:**父先于子**(BFS 天然保证)、**确定性**(同层顺序由 `VolumeSnapshotTree` 内部保证)、**稳定性**(纯粹由 uuid + parentUuid 决定)。 + +**循环引用防护**:`VolumeSnapshotTree.fromVOs()` 内部以 `parentUuid` 构建有向图。若数据库中快照链存在循环引用(如 A→B→C→A,属于数据库层面的非法数据),BFS 遍历不会访问环上节点(无入度为 0 的根节点可达这些节点)。这些"孤立环"节点将不会出现在 `levelOrderTraversal()` 输出中。处理策略:构建完成后比对输出数量与输入数量——若 `result.size() < allSnapshots.size()`,说明存在不可达快照(环或孤立节点),记录 WARN 日志 `"Unreachable snapshots detected for VM {vmUuid}: {count} out of {total}, possible circular reference"` 并将遗漏的快照按 uuid ASC 追加到结果尾部(保证不丢数据,注册时依赖 FK 而非顺序重建关系)。 + +**排序安全性说明**:BFS 拓扑排序仅改变输出顺序,不改变节点内容。`parentUuid/parentId` 关系通过快照 VO 字段完整保留,注册恢复时不会发生层级信息丢失。 + +> **复用已有基础设施**:`VolumeSnapshotTree`([VolumeSnapshotTree.java](../header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java))已有完整的树构建和 BFS 层序遍历实现,无需重新实现。 + +```java +// 实际实现:复用 VolumeSnapshotTree +List topoSort(List allSnapshots) { + // 先按 volumeUuid 分组,再按 treeUuid 分组(双层 TreeMap 保证 ASC 排序) + Map>> byVolumeThenTree = + allSnapshots.stream().collect(Collectors.groupingBy( + VolumeSnapshotVO::getVolumeUuid, TreeMap::new, + Collectors.groupingBy(VolumeSnapshotVO::getTreeUuid, + TreeMap::new, Collectors.toList()))); + + List result = new ArrayList<>(); + for (Map> treesInVolume : byVolumeThenTree.values()) { + for (List treeSnapshots : treesInVolume.values()) { + VolumeSnapshotTree tree = VolumeSnapshotTree.fromVOs(treeSnapshots); + List ordered = tree.levelOrderTraversal(); + for (VolumeSnapshotInventory inv : ordered) { + result.add(findByUuid(treeSnapshots, inv.getUuid())); + } + } + } + return result; +} +``` + +### 2.2 VmMetadataCategory 枚举 + +> **权威定义**:此枚举同时适用于 Java DTO 和 sblk Header VM 摘要区。Part 4b Header 中 `VmCategory` 字段的取值与此枚举一一对应。 + +```java +public enum VmMetadataCategory { + REGULAR, // 0 — 普通虚拟机(含链式克隆子 VM) + TEMPLATE, // 1 — 模板虚拟机(TemplatedVmInstanceVO 存在) + TEMPLATE_CACHE // 2 — 模板缓存虚拟机(TemplatedVmInstanceCacheVO 中的 cacheVmInstanceUuid) +} +``` + +| 类型 | 判定条件 | 写入元数据 | 注册行为 | +|------|----------|:---:|----------| +| `REGULAR` | 非模板、非缓存的所有 VM | (Y) | 正常注册 | +| `TEMPLATE` | `TemplatedVmInstanceVO` 存在 | (Y) | 注册为普通 VM(不恢复模板身份) | +| `TEMPLATE_CACHE` | `TemplatedVmInstanceCacheVO.cacheVmInstanceUuid` 匹配 | (Y) | **拒绝注册**(返回 `METADATA_CACHE_VM_NOT_REGISTERABLE`) | + +**扩展约束**:枚举在存储/传输层按 **int** 语义处理,当前占用值 `0~2`,预留 `3~99` 给未来类别扩展,避免与历史版本冲突。 + +**向后兼容策略**:新增枚举值时(如 v2+ 加入 `APPLIANCE = 3`),旧版本 Agent 读取到未知 int 值应按 `REGULAR` 降级处理(安全默认值)。Java 端 Gson 反序列化遇到未知枚举值返回 null,代码中对 `vmCategory == null` 已统一视为 `REGULAR`。因此无需额外版本协商,仅需在新增枚举时更新此注释标注已占用值。 + +**构建时判定逻辑**(先判缓存再判模板): + +```java +if (Q.New(TemplatedVmInstanceCacheVO.class) + .eq(TemplatedVmInstanceCacheVO_.cacheVmInstanceUuid, vmUuid) + .isExists()) { + dto.vmCategory = VmMetadataCategory.TEMPLATE_CACHE; +} else if (Q.New(TemplatedVmInstanceVO.class) + .eq(TemplatedVmInstanceVO_.uuid, vmUuid) + .isExists()) { + dto.vmCategory = VmMetadataCategory.TEMPLATE; +} else { + dto.vmCategory = VmMetadataCategory.REGULAR; +} +``` + +### 2.3 ResourceMetadata + +``` +ResourceMetadata +├── resourceUuid: String // 资源 UUID(冗余,必须与 vo 内部 uuid 一致) +├── vo: String // VO 全量 JSON 明文 +├── systemTags: String // 白名单过滤后的 SystemTagVO JSON 列表 Base64 编码 +└── resourceConfigs: String // 白名单过滤后的 ResourceConfigVO JSON 列表 Base64 编码 +``` + +### 2.4 VolumeResourceMetadata + +`VolumeResourceMetadata` 继承 `ResourceMetadata`,额外携带该卷的引用数据: + +``` +VolumeResourceMetadata extends ResourceMetadata +├── (inherited: resourceUuid, vo, systemTags, resourceConfigs) +├── snapshotReferences: List // 该卷的 VolumeSnapshotReferenceVO JSON 列表 +└── snapshotReferenceTrees: List // 该卷关联的 VolumeSnapshotReferenceTreeVO JSON 列表 +``` + +> **快照数据归属**:快照(`VolumeSnapshotVO`)提升到 `VmInstanceMetadataDTO.snapshots` 扁平列表,与 `snapshotGroups`/`snapshotGroupRefs` 同级。 +> 引用数据(`snapshotReferences`/`snapshotReferenceTrees`)仍保留在 `VolumeResourceMetadata` 内,因为引用关系与具体卷紧密绑定。 +> 注册时 ReferenceVO 的全局拓扑排序(按 parentId 依赖)从各 volume 收集后统一处理。 + +**快照提升到 DTO 层的设计理由**: + +| # | 好处 | 详细说明 | +|---|------|----------| +| 1 | **注册恢复零额外操作** | 注册时先 persist 所有 VolumeVO → 再遍历 `snapshots` 列表逐条 persist VolumeSnapshotVO,`volumeUuid` FK 天然满足。若放在 Volume 层,需先解包每个 Volume 的 snapshots 再逐卷恢复,多一层循环嵌套。 | +| 2 | **与 snapshotGroups 同构** | `snapshotGroups`(VolumeSnapshotGroupVO)是天然 VM 级别概念(一个 group 横跨多卷)。snapshots 放同级后,三个快照相关列表在同一层级统一管理,结构对称。 | +| 3 | **全局拓扑排序一次完成** | 快照链变基(sblk rebase)需要全局拓扑顺序。扁平列表直接做一次 BFS 拓扑排序即可,无需先合并再排序。 | +| 4 | **一致性检查简化** | 对比 DB 与存储上的快照时,直接比对两个扁平列表。无需逐 Volume 打开再逐一比对。 | +| 5 | **Payload 构建效率** | Builder 一次 `SELECT * FROM VolumeSnapshotVO WHERE volumeUuid IN (...)` 查出所有快照,序列化为一个列表。 | +| 6 | **VolumeResourceMetadata 保持精简** | Volume 层只保留与本卷强绑定的数据(ReferenceVO/ReferenceTreeVO)。快照通过 `volumeUuid` 字段自带归属,无需冗余嵌套。 | + +> **代价**:失去 Volume→Snapshot 的直观嵌套结构。但每条 `VolumeSnapshotVO` 自带 `volumeUuid`,注册时 `groupBy(volumeUuid)` 即可按卷分组,O(N) 遍历,可忽略。 + +| VO | 含义 | 注册意义 | +|----|------|----------| +| `VolumeSnapshotReferenceVO` | 记录链式克隆时子 VM 卷对缓存 VM 快照的依赖关系 | 不恢复会导致快照引用计数为 0,子 VM 执行 flatten/删除时无法正确清理物理快照文件 | +| `VolumeSnapshotReferenceTreeVO` | 引用树根节点,记录底层快照链的根信息 | 完全独立表(零 FK 约束),维护引用链路的树形结构 | + +**VolumeSnapshotReferenceVO 查询范围说明(讨论澄清)**:构建 `VolumeResourceMetadata.snapshotReferences` 时,查询条件为 `WHERE referenceVolumeUuid = 当前 VM 的卷 UUID`(即子 VM 自身的卷 UUID),而非 `volumeUuid`(缓存 VM 的卷)。原因:一个 VM 只关心自己的引用记录,不需要包含其他 VM 对同一缓存快照的引用。`referenceVolumeUuid` 是 FK → `VolumeEO`,指向子 VM 的卷。 + +**关键 FK 约束**(基于 `V4.7.0__schema.sql` DDL): + +| 字段 | FK 目标 | ON DELETE | 含义 | +|------|---------|-----------|------| +| `ReferenceVO.referenceVolumeUuid` | `VolumeEO` | CASCADE | 子 VM 卷删除时级联删除引用记录 | +| `ReferenceVO.parentId` | 自身 `id` | SET NULL | 父引用删除后置 NULL | +| `ReferenceVO.treeUuid` | `ReferenceTreeVO` | SET NULL(DDL 实际值) | 树删除后置 NULL | +| `ReferenceVO.volumeUuid` | — | **无 FK** | 允许指向已删除的缓存 VM 卷 | +| `ReferenceVO.volumeSnapshotUuid` | — | **无 FK** | 允许指向已删除的缓存 VM 快照 | +| `ReferenceTreeVO.*` | — | **零 FK** | 完全独立表,所有字段均无外键约束 | + +--- + +## 3. 编码策略 — per-Resource 字段级 Base64 + +**设计决策**:DTO 整体为明文 JSON 写入存储介质,**不对整体 JSON 做 Base64 编码**。仅对每个 `ResourceMetadata` 中的 `systemTags` 和 `resourceConfigs` 字段采用 **per-Resource 整体 Base64 编码**。 + +- **sblk**:Slot Payload = DTO JSON 明文 +- **local/NFS**:文件内容 = DTO JSON 明文 + +**理由**: + +1. 避免整体 Base64 带来的 4/3 空间膨胀 +2. SystemTag/ResourceConfig 可能含特殊字符,Base64 避免 JSON 转义问题 +3. 主体数据保持明文,`APIReadVmInstanceMetadataFromPrimaryStorageMsg` 可直接读取完整 JSON +4. 一致性检查 API 可直接对主体数据做结构化比较 +5. per-Resource 整体编码减少 Base64 header 开销和解码次数 + +**容量说明**:Base64 对原始数据体积膨胀约 **33%**。当前方案仅对 `systemTags/resourceConfigs` 做字段级编码,整体可控。 + +**编解码流程**: + +``` +写入: + 对每个 ResourceMetadata: + filteredTags = 按白名单过滤 List + filteredConfigs = 按白名单过滤 List + systemTags = Base64( JSON.toJsonString( sorted(filteredTags, by uuid) ) ) + resourceConfigs = Base64( JSON.toJsonString( sorted(filteredConfigs, by uuid) ) ) + DTO → JSON → 写入存储 + +读取: + 存储 → JSON → DTO + 对每个 ResourceMetadata: + List = JSON.parseArray( Base64Decode(systemTags) ) + List = JSON.parseArray( Base64Decode(resourceConfigs) ) +``` + +--- + +## 4. 序列化关注点 + +| 关注点 | 方案 | +|--------|------| +| 嵌套 JSON 转义 | 保持 String 类型,Gson 自动处理双重转义/反转义 | +| JSON 字段顺序一致性 | 所有 DTO 字段使用 `@SerializedName` 注解显式命名并按声明顺序输出。**设计决策理由(讨论 Δ-序列化)**:纯依赖 Java 字段声明顺序在重构时有序变风险,`@SerializedName` 固化字段名使 JSON key 不受 Java 重命名影响,同时 Gson 按声明顺序输出已满足确定性要求,无需额外 `@Order` 注解 | +| null 字段处理 | Gson 默认跳过 null 字段(`new Gson()` 不输出 null),反序列化时 Java 默认值与 null 语义一致。**设计决策理由(讨论 Δ-null)**:DTO 中值为 null 的字段在序列化后的 JSON 中不存在对应 key,反序列化时 Java 字段保持声明默认值(引用类型为 null,基本类型为 0/false),语义等价,无需特殊处理 | +| SystemTag/ResourceConfig 写入策略 | 构建时按白名单过滤(见 §4.1),仅写入注册时需要恢复的 tag/config | +| 元数据大小 | 极端场景(24盘×256快照)约 5-10MB,仅 SystemTag/ResourceConfig 字段 Base64 编码,整体膨胀可忽略,在 sblk 单 Slot 32MB 内 | +| 不压缩的理由 | 正常场景 <100KB,极端场景罕见;压缩会增加 Agent 依赖和调试复杂度;未来可通过 bump HeaderVersion 引入 CompressionType 字段支持 | +| VO JSON 字段范围 | 包含所有非 `@Transient` 持久化字段;注册时 `id`(自增主键)由 DB 重新生成,`createDate` 保留原值,`lastOpDate` 替换为注册时间 | + +### 4.1 SystemTag/ResourceConfig 构建时过滤规则 + +**构建时:白名单过滤** + +序列化时按白名单过滤,仅将影响 VM 注册恢复的 SystemTag 和 ResourceConfig 写入元数据。 + +1. 白名单复用已有的 `CoreMemorySnapshotConfigs`(内存快照恢复功能维护的白名单,见下方),影响 VM XML 的 tag 和 config +2. 初始 SystemTag 白名单:`USERDATA`、`SSHKEY`、`BOOT_MODE`、`HOSTNAME`、`CPU_CORES`、`MACHINE_TYPE`、`VIRTIO` 等 +3. 初始 ResourceConfig 白名单:`NESTED_VIRTUALIZATION`、`VM_CPU_QUOTA`、`VM_CLOCK_TRACK`、`LIBVIRT_CACHE_MODE` 等 +4. 可通过 `@NeedRestoreOnVmApplySnapshot` 注解自动扩展白名单 +4. 未命中白名单的 SystemTag/ResourceConfig 不写入元数据 + +**注册时:直接恢复,无需二次过滤** + +> **设计决策**:元数据中的 SystemTag 和 ResourceConfig 已在构建时经过白名单过滤,注册恢复时直接持久化到 DB,**不再执行二次过滤**。 + +**理由**: +1. 构建时已过滤 → 元数据中只包含影响 VM XML 的 tag/config +2. 二次过滤无意义——白名单是同一套规则 +3. 升级后白名单扩展时,已有 full-refresh 覆盖全量元数据 + +**白名单定义**(复用已有的 `CoreMemorySnapshotConfigs`,无需重复维护): + +> 内存快照恢复功能已有完整的 SystemTag / ResourceConfig 白名单定义(见 `CoreMemorySnapshotConfigs.java`),元数据功能直接复用,保证两个场景的白名单始终一致。 + +```java +// 元数据构建时直接使用 CoreMemorySnapshotConfigs 的白名单过滤: +// SystemTag 过滤: +CoreMemorySnapshotConfigs.restoreCandidatePatternedSystemTags // PatternedSystemTag 列表 +CoreMemorySnapshotConfigs.restoreCandidateSystemTags // SystemTag 列表 + +// ResourceConfig 过滤——按资源类型分组: +CoreMemorySnapshotConfigs.vmRestoreCandidateConfigs // VM 级别的 GlobalConfig +CoreMemorySnapshotConfigs.volumeRestoreCandidateConfigs // Volume 级别的 GlobalConfig +CoreMemorySnapshotConfigs.vmNicRestoreCandidateConfigs // VmNic 级别的 GlobalConfig + +// 新增白名单条目两种方式: +// 1. 在 CoreMemorySnapshotConfigs 静态列表中直接添加 +// 2. 在 GlobalConfig 字段上标注 @NeedRestoreOnVmApplySnapshot 注解(自动收集) +``` + +> 新增白名单条目 → 修改 `CoreMemorySnapshotConfigs` 或添加 `@NeedRestoreOnVmApplySnapshot` 注解。内存快照恢复和元数据注册两个场景同步受益。 +> CI 可维护性保证:`MetadataWhitelistChecker`(统一 CI 检查)见 [Part 1b §3](vm-metadata-01b-API拦截与VM解析.md#3-统一-ci-检查--metadatawhitelistchecker)。 + +**演进说明(讨论澄清)**:`CoreMemorySnapshotConfigs` 当前命名绑定内存快照恢复场景。随着元数据功能上线,建议后续将其重构为更通用的命名(如 `VmConfigRestoreCandidates` 或 「影响虚拟机 XML 的配置」),统一表达「影响 VM 运行时配置、需要在恢复场景中还原的 Tag/Config 列表」语义。重构仅涉及类名和引用点,不改变白名单内容和收集逻辑。 + +**演进备注**:`buildVmInstanceMetadata()` 当前查询源列表为显式实现,v2+ 可提取为 SPI(如 `VmMetadataBuildSource`)以支持插件化扩展额外 VO/资源采集源。 + +--- + +## 5. 反序列化关注点 + +| 关注点 | 方案 | +|--------|------| +| 二步反序列化 | 先反序列化 DTO,再反序列化各 ResourceMetadata.vo 为具体 VO 类 | +| resourceUuid 一致性校验 | 反序列化后校验 `resourceMetadata.resourceUuid == parsedVO.getUuid()` | +| VO 字段版本兼容(多字段) | Gson 忽略缺失字段,填充 Java 默认值 | +| VO 字段版本兼容(少字段) | Gson 忽略未知字段 | +| Base64 解码失败 | systemTags/resourceConfigs 元素 Base64 解码失败时直接报错拒绝注册,不做部分恢复 | + +--- + +## 6. schemaVersion 版本规则 + +### 6.1 版本号定义 + +`schemaVersion` 使用 ZStack 数据库版本号,即 **`dbf.getDbVersion()`** 的返回值,与数据库 schema 版本完全一致。 + +**版本号格式说明(讨论澄清)**:`dbf.getDbVersion()` 返回纯数字版本号(如 `"4.7.0"`),不含 `V` 前缀或 `__schema` 等后缀。该值直接来源于 `DatabaseFacade` 的版本查询,调用方无需额外清洗或裁剪。 + +### 6.2 比较规则 + +> **统一规则**:注册 API 和预检查 API 均使用精确匹配:`metadata.schemaVersion == dbf.getDbVersion()`。 + +| 场景 | 行为 | +|------|------| +| `metadata.schemaVersion == dbf.getDbVersion()` | 匹配,正常注册 | +| 不匹配 + `forceVersionMismatch=false` | 拒绝(`METADATA_SCHEMA_VERSION_MISMATCH`) | +| 不匹配 + `forceVersionMismatch=true` | 允许注册,缺失字段置 null,warnings 记录 | + +**`forceVersionMismatch=true` 行为精确定义(讨论澄清)**:该标志**仅跳过 schemaVersion 精确匹配检查**,所有其他校验(UUID 冲突、installPath 存在性、readStatus 可用性、跨存储、vmCategory 类型等)均正常执行,不受此标志影响。缺失字段由 Gson 反序列化自动填充 Java 默认值(null/0/false),多余字段由 Gson 自动忽略。 + +### 6.3 版本生命周期 + +- 序列化时由 `VmMetadataBuilder` 自动填充 `dbf.getDbVersion()` +- 升级后通过全量刷新(批量 `markDirty`,Poller 自动处理)将所有 VM 元数据更新到新版本 +- 刷写端不依赖旧版本元数据,从 DB 直接构建新版本元数据覆盖写入 + +### 6.4 升级时间窗口 + +- 升级后批量 `markDirty` 所有已启用元数据的 VM,Poller 自动分批处理(详见 [Part 2b §9](vm-metadata-02b-高可用与运维.md#9-升级后全量刷新)) +- 10 万 VM × 平均 50ms/VM ≈ 5000 秒 ≈ 83 分钟 +- 窗口期内若需注册 VM,可使用 `APIUpdateVmMetadataMsg` 单独更新指定 VM 的元数据 + +### 6.5 vmCategory 兼容性 + +`vmCategory` 是新增字段,需要 bump `schemaVersion`: +- **新版本写入的元数据**:包含 `vmCategory` 字段 +- **旧版本写入的元数据**:不含该字段,Gson 反序列化时 `vmCategory` 为 `null` +- **注册时处理**:`vmCategory == null` 视为 `REGULAR`(向后兼容) + +--- + +## 7. VmCdRomVO 等附属资源 + +当前版本不纳入元数据。理由: + +- CD-ROM 挂载状态通常不影响 VM 恢复启动 +- USB/PCI 透传设备与宿主机绑定,跨环境无意义 +- 后续版本如需支持,通过 `VmInstanceMetadataDTO` 新增字段 + bump schemaVersion diff --git "a/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" "b/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" new file mode 100644 index 00000000000..98a11eef395 --- /dev/null +++ "b/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" @@ -0,0 +1,433 @@ +# VM 元数据 — API 拦截与 VM 解析 + +## 目录 + +1. [@MetadataImpact 注解与 API 拦截](#1-metadataimpact-注解与-api-拦截) + - [1.5 条件性影响(v2+)](#15-条件性影响v2) + - [1.6 内部消息覆盖策略](#16-内部消息覆盖策略) + - [1.7 pendingApis 生命周期治理](#17-pendingapis-生命周期治理) +2. [VmUuid 解析器(Resolver)](#2-vmuuid-解析器resolver) + - [2.4 Resolver → API 映射表](#24-resolver--api-映射表) +3. [统一 CI 检查 — MetadataWhitelistChecker](#3-统一-ci-检查--metadatawhitelistchecker) + - [3.1 CI 扩展:内部消息 markDirty 审计](#31-ci-扩展内部消息-markdirty-审计) + - [3.2 设计决策:为什么不用 ExtensionPoint](#32-设计决策为什么不用-extensionpoint-监听-tagconfig-变更) +4. [影响虚拟机元数据的 API 清单](#4-影响虚拟机元数据的-api-清单) +5. [约束与不変量](#5-约束与不変量) + +--- + +## 1. @MetadataImpact 注解与 API 拦截 + +### 1.1 注解定义 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface MetadataImpact { + MetadataImpactLevel value() default MetadataImpactLevel.CONFIG; + Class resolver() default DirectVmUuidResolver.class; + boolean updateOnFailure() default false; +} + +public enum MetadataImpactLevel { + NONE, // 无关虚拟机配置 + CONFIG, // 虚拟机普通配置更新 + STORAGE // 虚拟机存储结构更新(快照/存储迁移等) +} +``` + +### 1.2 拦截范围(重要) + +**所有 `APIMessage` 子类必须标注 `@MetadataImpact`,CI 强制检查。** + +1. `MetadataWhitelistChecker` 扫描所有 `APIMessage` 子类,**未标注 `@MetadataImpact` 的 API 直接 CI 报错** +2. 开发者必须为每个 API 显式赋值 `NONE`、`CONFIG` 或 `STORAGE` +3. 运行时拦截器只对 level ≠ `NONE` 的 API 触发元数据更新 + +**opt-out 策略说明**(存量 API 标注): +- `@MetadataImpact(NONE)` — 与 VM 配置无关的 API(网络管理、用户管理等) +- `@MetadataImpact(STORAGE)` — 涉及存储拓扑变更的 API(快照、存储迁移、卷加卸等) +- `@MetadataImpact` 或 `@MetadataImpact(CONFIG)` — 其余 VM 相关配置更新 API + +### 1.3 STORAGE 标记的精确定义 + +`@MetadataImpact(STORAGE)` 表示该 API 导致 VM 的**存储结构**发生变化。以下**任一条件**成立即判定: + +| # | 条件 | 典型 API | +|---|------|----------| +| 1 | VM 的卷列表发生变化(数量增减) | Attach/Detach/DeleteDataVolume | +| 2 | 任一卷的 installPath 发生变化 | StorageMigrate, Reimage, Flatten | +| 3 | 任一卷的快照数量发生变化 | Create/DeleteSnapshot, SnapshotGroup | + +**不属于存储结构变化的场景**:卷大小变更(resize)—— installPath 不变、快照不变、卷数不变,归类为 `CONFIG`。 + +**对 sblk 的影响**:`@MetadataImpact(CONFIG)` → OP type=1(`CONFIG_UPDATE`),`@MetadataImpact(STORAGE)` → OP type=2(`STORAGE_CHANGE`)。OP type 通过 `storageStructureChange` 字段贯穿整条消息链。详见 [Part 4c §2](vm-metadata-04c-sblk写入流程.md#2-核心流程三阶段原子写入)。 + +### 1.4 updateOnFailure 触发条件 + +| 条件 | 是否触发 | +|------|----------| +| `updateOnFailure=true` + API 成功 | 触发 | +| `updateOnFailure=true` + API 失败(实际执行了部分逻辑) | 触发 | +| `updateOnFailure=true` + API 参数校验失败(未进入业务逻辑) | **不触发** | +| `updateOnFailure=false` + API 失败 | 不触发 | + +**关键实现 — `__metadata_vmUuids__` 的设置时机**: + +`__metadata_vmUuids__` 是 ThreadContext 中的标记键,由 `BeforeDeliveryMessageInterceptor` 在 API 参数校验通过后设置。其存在性即"是否进入过业务逻辑"的可靠标志。纯参数校验失败不经过拦截器,ThreadContext 中无此键,自然不触发 `updateOnFailure`。 + +``` +时序流程: + CloudBus 收到 APIMessage + ├─ 参数校验失败 → 直接返回错误 → ThreadContext 无 __metadata_vmUuids__ + └─ 参数校验通过 → 进入 BeforeDeliveryMessageInterceptor + ├─ @MetadataImpact level ≠ NONE → Resolver.resolve(msg) → 存入 ThreadContext + pendingApis + └─ API 业务逻辑执行 + ├─ 成功 → beforePublishEvent → markDirty + └─ 失败 → 检查 updateOnFailure + __metadata_vmUuids__ → 满足则 markDirty +``` + +### 1.5 条件性影响(v2+) + +Q1b-5 结论:现阶段保持 `@MetadataImpact` 简单枚举语义(`NONE/CONFIG/STORAGE`),不在 v1/v1.1 引入条件表达式。 + +| 项 | v1/v1.1 决策 | v2+ 预留 | +|----|--------------|----------| +| 注解模型 | 固定 `MetadataImpactLevel` + 固定 Resolver | 可扩展 `condition` 或策略接口 | +| 风险控制 | 采用保守标注:不确定场景优先 `CONFIG`/`STORAGE` 而非 `NONE` | 引入运行时条件判定避免过度刷新 | +| 文档约束 | 任何“条件性”需求先落入 API 评审清单并记录原因 | 条件模型落地需单独 RFC | + +### 1.6 内部消息覆盖策略 + +`@MetadataImpact` 仅覆盖 `APIMessage`。对内部消息(`Message` 子类)采用**显式注册 + 代码审计**: + +```java +private static final Set> INTERNAL_METADATA_MESSAGES = Set.of( + AllocateHostMsg.class, + MigrateVmMsg.class, + ChangeVmIpMsg.class, + DetachDataVolumeFromVmMsg.class, + DeleteVolumeSnapshotMsg.class + // 仅示例:实际清单以 vm-instance / storage / network 相关处理器审查结果为准 +); +``` + +- 注册表用于声明“已知会影响元数据且不经过 API 拦截器”的消息类型。 +- 对应 handler 在事务提交后调用 `markDirty()`,避免 dirty mark 指向未提交快照。 +- Poller 的 stale 窗口为设计内窗口,不消除;通过“提交后 markDirty + 下一轮全量构建”收敛(见 [Part 2 §1.4](vm-metadata-02-脏标记与Poller.md#14-最终一致性模型))。 + +### 1.7 pendingApis 生命周期治理 + +`pendingApis` 为 `ConcurrentHashMap`,新增超时治理(超时时间可通过 GlobalConfig 配置),防止 API 超时导致 entry 泄漏: + +1. 每 5 分钟执行一次清理任务。 +2. 移除“创建时间 > `VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES`(默认 45 分钟)”的 entry。 +3. 对被清理 entry 的 vmUuid 执行 `markDirty()`(最终一致)。 +4. `afterCompletion` 增加 null check:`remove(apiId)` 返回 null 时按“已被清理”分支继续,不报错。 + +**Per-API 超时策略(讨论 Δ-3)**:原方案使用固定 45 分钟超时,无法匹配 API 类型差异。改为从 `PendingApiContext` 中记录 API 类名,清理时根据 API 类型动态计算超时: +- 普通 API:使用 API 自身的 `timeout` 字段(若该 API 为 LongJob 触发,则取 LongJob 超时)。 +- 回退默认值:若 API 无显式 timeout 配置,使用 `VM_METADATA_PENDING_API_TIMEOUT_MINUTES`(默认 45min)。 +- 此设计确保 LongJob 场景(如存储迁移可达数小时)不会被过早清理,同时普通短 API 不会等待过久才触发 markDirty。 + +**MN 重启时 pendingApis 丢失的处理策略**:`pendingApis` 是 JVM 内存结构,MN 重启后全部丢失。对正在执行中的 API,有以下几种情况: +- **API 执行已到达 `afterCompletion`**:`remove(apiId)` 返回 null → 按"已清理"分支处理 → 对 vmUuids 执行 `markDirty()`,保证最终一致。由于 MN 已重启,此路径不会执行。 +- **API 尚未完成即 MN 崩溃**:API 执行被中断,`markDirty()` 未被调用。恢复依赖两条路径:(1) 用户重新发起 API → 新的 API 触发 `markDirty()`;(2) 路径指纹巡检(Part 2b §8.2)发现漂移 → 自动 `markDirty()`。 +- **结论**:MN 重启丢失 pendingApis 不会导致数据永久不一致,最终一致性由 Poller + 路径巡检保证。无需持久化 pendingApis(持久化成本高于收益)。 + +**`updateOnFailure` 与 pendingApis 的交互**:`updateOnFailure=true` 的 API 在失败时通过 `afterCompletion(reply)` 回调处理。回调从 `pendingApis.remove(apiId)` 取出预缓存的 vmUuids,检查 `reply.isSuccess()` 为 false,若 `updateOnFailure=true` 则执行 `markDirty()`。与成功路径使用同一 pendingApis entry,无额外数据结构。若 entry 已被超时清理,`remove()` 返回 null,此时 vmUuids 已在清理时被 `markDirty()` 过,不会遗漏。 +```java +scheduledPool.scheduleAtFixedRate(() -> { + Instant deadline = Instant.now().minus(Duration.ofMinutes(VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES.value(Long.class))); + pendingApis.entrySet().removeIf(e -> { + if (e.getValue().getCreateTime().isBefore(deadline)) { + e.getValue().getVmUuids().forEach(vm -> markDirty(vm, e.getValue().isStorageStructureChange())); + return true; + } + return false; + }); +}, 5, 5, TimeUnit.MINUTES); +``` + +--- + +## 2. VmUuid 解析器(Resolver) + +### 2.1 解析时机 + +Resolver 在**两个时机**捕获 vmUuid: + +1. **API 执行前**(`BeforeDeliveryMessageInterceptor`):预解析 vmUuid 并缓存到 `pendingApis` ConcurrentHashMap 中(key = apiId) +2. **API 成功后**(`beforePublishEvent`):从 `pendingApis` 读取缓存的 vmUuid,调用 `markDirty(vmUuid)` 标脏 + +**执行线程说明**:`beforeDeliveryMessage()` 在消息投递线程中同步执行。Resolver 的 DB 查询在此线程中执行,对单次 API 延迟影响极小(<1ms)。 + +### 2.2 Resolver 接口 + +```java +public interface VmUuidFromApiResolver { + List resolve(APIMessage msg); +} +``` + +### 2.3 内置 Resolver 实现 + +| Resolver | 逻辑 | +|----------|------| +| `DirectVmUuidResolver` | 从 `msg.getVmInstanceUuid()` 直接获取 | +| `VolumeToVmResolver` | 通过 volumeUuid 查 `VolumeVO.vmInstanceUuid` | +| `PreCaptureVolumeToVmResolver` | 同 VolumeToVmResolver,标记为需要预捕获(API 执行前获取) | +| `SnapshotToVmResolver` | snapshotUuid → VolumeSnapshotVO.volumeUuid → VolumeVO.vmInstanceUuid | +| `SnapshotGroupToVmResolver` | groupUuid → refs → 多个 volumeUuid → 多个 vmUuid | +| `ResourceUuidToVmResolver` | resourceUuid 可能是 VM/Volume/NIC,逐一判断 | + +**`ResourceUuidToVmResolver` 过滤非 VM 相关资源**:`APICreateSystemTagMsg` 等 Tag API 的 `resourceUuid` 可能指向任意资源类型(Host、Zone、L3Network 等)。`ResourceUuidToVmResolver` 的实现按以下优先级解析: +1. `dbf.findByUuid(resourceUuid, VmInstanceVO.class)` → 非 null 则直接返回 vmUuid +2. `dbf.findByUuid(resourceUuid, VolumeVO.class)` → 取 `vmInstanceUuid` +3. `dbf.findByUuid(resourceUuid, VmNicVO.class)` → 取 `vmInstanceUuid` +4. 以上均为 null → 返回空列表(该 Tag 不关联 VM,跳过 markDirty) + +此为已有实现的显式文档化。每步查询命中索引,开销 < 1ms。非 VM 相关 Tag(如 Host Tag)在第 4 步返回空,不触发任何元数据操作。 + +| `NicToVmResolver` | nicUuid → VmNicVO.vmInstanceUuid | +| `PreCaptureNicToVmResolver` | 同 NicToVmResolver,标记为需要预捕获 | + +### 2.4 Resolver → API 映射表 + +该表用于代码评审和 CI 问题定位;权威 API 列表以 §4 为准。 + +| Resolver | 典型 API | 说明 | +|----------|----------|------| +| `DirectVmUuidResolver` | `APIUpdateVmInstanceMsg`、`APISetVmBootOrderMsg`、`APIReimageVmInstanceMsg`、`APICloneVmInstanceMsg` | API 入参直接包含 vmUuid | +| `VolumeToVmResolver` | `APIAttachDataVolumeToVmMsg`、`APIRecoverDataVolumeMsg`、`APIPrimaryStorageMigrateVolumeMsg`、`APIResizeDataVolumeMsg` | 通过 volumeUuid 反查 VM | +| `PreCaptureVolumeToVmResolver` | `APIDetachDataVolumeFromVmMsg`、`APIDeleteDataVolumeMsg` | 删除/卸载场景需 API 前预捕获 | +| `SnapshotToVmResolver` | `APIDeleteVolumeSnapshotMsg`、`APIRevertVolumeFromSnapshotMsg` | snapshotUuid → volumeUuid → vmUuid | +| `SnapshotGroupToVmResolver` | `APIDeleteVolumeSnapshotGroupMsg` | groupUuid 可映射多个 VM(跨卷) | +| `ResourceUuidToVmResolver` | `APICreateSystemTagMsg`、`APIDeleteTagMsg`、`APIUpdateResourceConfigMsg` | 资源类型可能是 VM/Volume/NIC,需多分支解析 | +| `NicToVmResolver` | `APIChangeVmNicNetworkMsg`、`APIChangeVmNicStateMsg`、`APIDetachNicFromBondingMsg` | nicUuid → vmUuid | +| `PreCaptureNicToVmResolver` | `APIDeleteVmNicMsg` | 删除场景需 API 前预捕获 | + +--- + +## 3. 统一 CI 检查 — MetadataWhitelistChecker + +**注意**:此为唯一的 CI 检查类,合并了 API 注解检查、SystemTag 白名单检查、ResourceConfig 白名单检查。 +白名单数据复用 `CoreMemorySnapshotConfigs`(已有的内存快照恢复候选列表),不再另建 Provider 接口。 + +```java +public class MetadataWhitelistChecker extends PostBuildCheckerCase { + @Override + public void check() { + // Part 1: API @MetadataImpact 注解 + Resolver 检查 + Set> allApiMsgs = BeanUtils.reflections.getSubTypesOf(APIMessage.class); + for (Class msgClass : allApiMsgs) { + if (isQueryOrGetApi(msgClass)) continue; + assertMetadataImpactPresent(msgClass); // 注解必须存在 + assertResolverValid(msgClass); // level ≠ NONE 时检查 Resolver + } + + // Part 2: SystemTag 白名单检查(数据来源:CoreMemorySnapshotConfigs) + Set allDefinedTags = scanAllSystemTagDefinitions(); + Set registeredTags = new HashSet<>(); + registeredTags.addAll(toTagNames(CoreMemorySnapshotConfigs.restoreCandidatePatternedSystemTags)); + registeredTags.addAll(toTagNames(CoreMemorySnapshotConfigs.restoreCandidateSystemTags)); + // @NeedRestoreOnVmApplySnapshot 注解标注的 Tag 自动纳入 + registeredTags.addAll(collectAnnotatedTags(NeedRestoreOnVmApplySnapshot.class)); + for (String tag : allDefinedTags) { + if (!registeredTags.contains(tag)) { + fail("SystemTag '" + tag + "' not in CoreMemorySnapshotConfigs whitelist"); + } + } + + // Part 3: ResourceConfig 白名单检查(数据来源:CoreMemorySnapshotConfigs) + Set allConfigCategories = scanAllResourceConfigCategories(); + Set registeredCategories = new HashSet<>(); + registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.vmRestoreCandidateConfigs)); + registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.volumeRestoreCandidateConfigs)); + registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.vmNicRestoreCandidateConfigs)); + for (String cat : allConfigCategories) { + if (!registeredCategories.contains(cat)) { + fail("ResourceConfig '" + cat + "' not in CoreMemorySnapshotConfigs whitelist"); + } + } + } +} +``` + +### 3.1 CI 扩展:内部消息 markDirty 审计 + +在现有三段检查基础上,补充 Part 4 审计(对 STORAGE 级内部消息为 ERROR 阻断级,其余为 WARNING): + +1. 扫描 `AbstractHandler` / `MessageHandler` 实现。 +2. 若检测到 VM 相关 VO(`VmInstanceVO`/`VolumeVO`/`VolumeSnapshotVO`/`VmNicVO`)写操作且未调用 `markDirty()`,输出 WARNING。 +3. 若 handler 处理的消息类型命中 `INTERNAL_METADATA_MESSAGES`,但未在注册表注释中标注触发来源,输出 WARNING。 +4. **STORAGE 级阻断**:若 handler 处理的消息类型命中 `INTERNAL_METADATA_MESSAGES` 且该消息在注册表中标注为 `STORAGE` 级别,但 handler 未调用 `markDirty()`,则**输出 ERROR 并阻断 CI 构建**(`fail()`),而非仅 WARNING。原因:STORAGE 级遗漏会导致 sblk OP type 错误,影响存储拓扑一致性,风险远高于 CONFIG 级遗漏。 + +**说明**:CONFIG 级内部消息的检查仍为"辅助发现"WARNING,不阻断 CI;STORAGE 级为强制阻断 ERROR。此区分确保高风险路径不被遗漏,同时避免对低风险基础设施路径(升级脚本、巡检修复)误杀。 + +**CI 报错引导示例**: +``` +"API APIAttachGpuDeviceToVmMsg 的 resolver GpuToVmResolver 未找到。 + 请实现 VmUuidFromApiResolver 接口,从该 API 消息中解析出关联的 vmUuid。 + 参考内置实现:NicToVmResolver、VolumeToVmResolver 等。" +``` + +--- + +### 3.2 设计决策:为什么不用 ExtensionPoint 监听 Tag/Config 变更? + +**背景**:ZStack 内部 SystemTag 的修改路径主要有三类: + +| 路径 | 频次 | 是否触发 lifecycle callback | +|------|------|---------------------------| +| `TagManager.newSystemTagCreator().create()` | ~263 处 | (Y) 触发 | +| `SystemTag.delete()` | ~143 处 | (Y) 触发 | +| `SQL.New(SystemTagVO.class)` / `dbf.persist(SystemTagVO)` | ~15 处 | (N) 完全绕过 | + +**结论:不额外引入 `SystemTagLifeCycleExtensionPoint`**,理由如下: + +1. **用户发起的 Tag/Config 修改全部通过 API**(§4.1/§4.2 已列出),API 拦截器已覆盖 +2. **内部 Tag 操作发生在已标注 `@MetadataImpact` 的 API 执行上下文中**(如 `APICreateVmInstanceMsg` 流程内的 `setBootMode` 系统标签),上层 API 已触发 `markDirty` +3. **~15 处直接 SQL 操作**均为基础设施级别(Host 重连写硬件信息、升级迁移脚本、IAM 操作),不涉及 `CoreMemorySnapshotConfigs` 中的元数据相关 Tag(USERDATA、SSHKEY、BOOT_MODE 等) +4. **即使极端情况遗漏**,Poller 安全网(Part 2 §4)从 DB 全量构建 DTO 写入,最终一致 +5. **ExtensionPoint 成本高**:需在 ~500+ 内部操作中逐一过滤白名单,不值得 + +ResourceConfig 同理:`ResourceConfig.updateValue()` 内部调用 ~92 处触发 extension,`SQL.New(ResourceConfigVO.class)` 直接操作仅 2 处(基础设施级别)。API 拦截 + Poller 安全网已足够。 + +--- + +## 4. 影响虚拟机元数据的 API 清单 + +以下列出所有需要标注 `@MetadataImpact` 且 level ≠ `NONE` 的 API。 + +### 4.1 SystemTag 相关 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APICreateSystemTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | +| `APIDeleteTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | +| `APIUpdateSystemTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | +| `APISetVmBootOrderMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIDeleteVmBootModeMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIDeleteVmSshKeyMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIDeleteVmHostnameMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APISetVmQgaMsg` | CONFIG | `DirectVmUuidResolver` | false | + +### 4.2 ResourceConfig 相关 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APIUpdateResourceConfigMsg` | CONFIG | `ResourceUuidToVmResolver` | false | +| `APIDeleteResourceConfigMsg` | CONFIG | `ResourceUuidToVmResolver` | false | + +### 4.3 磁盘加载卸载 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APIAttachDataVolumeToVmMsg` | STORAGE | `VolumeToVmResolver` | false | +| `APIDetachDataVolumeFromVmMsg` | STORAGE | `PreCaptureVolumeToVmResolver` | false | +| `APIDeleteDataVolumeMsg` | STORAGE | `PreCaptureVolumeToVmResolver` | false | +| `APIRecoverDataVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | +| `APIReimageVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | false | + +### 4.4 存储迁移 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APIPrimaryStorageMigrateVmMsg` | STORAGE | `DirectVmUuidResolver` | false | +| `APIPrimaryStorageMigrateVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | +| `APILocalStorageMigrateVolumeMsg` | STORAGE | `VolumeToVmResolver` | **true** | + +### 4.5 快照相关 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APICreateVolumesSnapshotMsg` | STORAGE | `VolumeToVmResolver` | false | +| `APICreateVolumeSnapshotGroupMsg` | STORAGE | `DirectVmUuidResolver` | **true** | +| `APIDeleteVolumeSnapshotMsg` | STORAGE | `SnapshotToVmResolver` | false | +| `APIDeleteVolumeSnapshotGroupMsg` | STORAGE | `SnapshotGroupToVmResolver` | **true** | +| `APIRevertVolumeFromSnapshotMsg` | STORAGE | `SnapshotToVmResolver` | false | +| `APIFlattenVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | + +**审计结论**:`APICreateVolumeSnapshotGroupMsg`、`APILocalStorageMigrateVolumeMsg`、`APIDeleteVolumeSnapshotGroupMsg` 为批量/部分成功风险 API,统一要求 `updateOnFailure=true`。 + +| API | 风险类型 | updateOnFailure 要求 | +|-----|----------|----------------------| +| `APICreateVolumeSnapshotGroupMsg` | 多卷快照,可能部分卷成功 | **true** | +| `APILocalStorageMigrateVolumeMsg` | 迁移流程分段执行,可能部分生效 | **true** | +| `APIDeleteVolumeSnapshotGroupMsg` | 组内快照删除可能部分成功 | **true** | + +### 4.6 克隆/模板 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APICloneVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | **true** | +| `APICreateTemplatedVmInstanceFromVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | **true** | +| `APICreateVmInstanceFromTemplatedVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | false | +| `APIExportImageFromBackupStorageMsg` | NONE | — | false | + +**说明**:Clone/Template 的 Resolver 解析**源 VM** UUID。新建 VM 的元数据由新建流程末尾自动生成。 + +**设计决策**:`APIExportImageFromBackupStorageMsg` 已确认为 `NONE`。导出镜像不修改 VM 配置/存储拓扑,不参与 resolver 解析与标脏链路。 + +### 4.7 模板虚拟机身份转换 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|------------------| +| `APIConvertVmInstanceToTemplatedVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIConvertTemplatedVmInstanceToVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | + +模板身份转换不改变存储拓扑,使用 CONFIG。元数据刷新时会重新计算 `vmCategory`。 + +### 4.8 卷大小变更 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|------------------| +| `APIResizeRootVolumeMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIResizeDataVolumeMsg` | CONFIG | `VolumeToVmResolver` | false | + +### 4.9 VM 配置变更 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|------------------| +| `APIUpdateVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false || `APIRecoverVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | + +**`APIRecoverVmInstanceMsg` 纳入说明(讨论补充)**:Recover VM 将 Destroyed 状态的 VM 恢复为 Stopped,该操作改变了 VM 状态但不涉及存储拓扑变更,因此标注为 `CONFIG`。Recover 后 VM 需要重新刷写元数据(Destroyed 状态期间元数据可能已被 Poller 删除)。 +### 4.10 网卡相关 + +| API | Level | Resolver | updateOnFailure | +|-----|-------|----------|-----------------| +| `APIChangeVmNicNetworkMsg` | CONFIG | `NicToVmResolver` | false | +| `APIAttachVmNicToVmMsg` | CONFIG | `DirectVmUuidResolver` | false | +| `APIChangeVmNicStateMsg` | CONFIG | `NicToVmResolver` | false | +| `APIDeleteVmNicMsg` | CONFIG | `PreCaptureNicToVmResolver` | false | +| `APIDetachNicFromBondingMsg` | CONFIG | `NicToVmResolver` | false | +| `APIAttachNicToBondingMsg` | CONFIG | `NicToVmResolver` | false | + +### 4.11 VM 创建与未纳入元数据的 API + +**VM 创建**:`APICreateVmInstanceMsg` 不通过 `@MetadataImpact` 拦截器触发。VM 创建 FlowChain 末尾直接调用 `initializeMetadata()` + `markDirty()`,确保元数据文件在 VM 创建成功后即被初始化和首次刷写。 + +**VM 创建元数据初始化时机(讨论澄清)**:元数据初始化采用异步 post-success hook 模式,即在 `CreateVmInstanceFlow` 主 Flow 全部成功后、返回 API 结果前,通过异步回调执行 `markDirty(vmUuid, true)`。失败不影响 VM 创建结果,Poller 安全网会在后续轮次重试。此设计避免元数据写入失败导致整个 VM 创建回滚。 + +**CD-ROM**:`APIDeleteVmCdRomMsg` 标注为 `@MetadataImpact(NONE)`。CD-ROM 当前版本不纳入元数据(见 [Part 1a §7](vm-metadata-01a-数据模型与序列化.md#7-vmcdromvo-等附属资源)),删除 CD-ROM 不触发元数据更新。 + +## 5. 约束与不変量 + +**`INTERNAL_METADATA_MESSAGES` 完备性保证**:静态注册表无法自动发现新增内部消息。保证完备性的手段为三层防线: +1. **开发规范**:修改 VM 存储拓扑字段的内部消息 handler,成功后必须调用 `markDirty()`(Part 2b §12.4 D1) +2. **CI Part 4**:`MetadataWhitelistChecker` 扫描 handler 实现中的 VO 写操作 + markDirty 调用(§3.1),STORAGE 级遗漏为 ERROR 阻断 +3. **路径指纹巡检**:运行时兜底,检测实际路径漂移并自动 markDirty(Part 2b §8.2) + +无需为 `INTERNAL_METADATA_MESSAGES` 引入自动发现机制。CI + 运行时双重防线已提供足够保障。 + +**非 KVM Hypervisor 排除**:`@MetadataImpact` 标注在 `APIMessage` 类层面,不区分 Hypervisor 类型。运行时拦截器在 Resolver 解析出 vmUuid 后,**通过查询 `VmInstanceVO.hypervisorType` 过滤**:仅 `KVM` 类型的 VM 继续标脏,其他类型(VMware、Simulator 等)静默跳过。此过滤在 `markDirty()` 入口处实现(与 `type != "UserVm"` 检查同层),不增加 Resolver 复杂度。非 KVM VM 的存储驱动不支持元数据格式(无 sblk LV / 无 `.zstack-vm-metadata` 目录),跳过是正确行为。 + +| 约束 ID | 约束描述 | 违反后果 | +|---------|----------|----------| +| C-IC | `INTERNAL_METADATA_MESSAGES` 与内部 handler 的 `markDirty()` 调用点必须一一可追溯,新增内部消息需同步更新注册表与注释来源 | 内部路径修改被遗漏,Poller 长期读旧状态 | +| C-IM | 所有 `APIMessage` 子类必须显式标注 `@MetadataImpact`(可为 NONE);`MetadataWhitelistChecker` 扫描全量子类,不允许“默认未声明” | 新增 API 逃逸拦截链,行为不可预测 | +| C-PA | `pendingApis` 必须具备超时清理(5min 周期、可配超时(默认 45min))与 afterCompletion null-safe 逻辑;清理时需补 `markDirty()` | 内存泄漏或超时 API 的最终一致性断裂 | +| C-RS | Resolver 选择需与 API 资源语义匹配;删除/卸载类 API 必须使用 pre-capture resolver 或等价机制 | API 完成后资源已消失,无法解析 vmUuid 导致漏标脏 | +| C-H1 | `INTERNAL_METADATA_MESSAGES` 中标注为 STORAGE 级别的内部消息,其 handler 必须调用 `markDirty()`;CI Part 4 对此类遗漏执行 ERROR 阻断(`fail()`) | STORAGE 级内部路径遗漏 markDirty,sblk OP type 错误,存储拓扑一致性断裂 | +| C-M4 | `pendingApis` 超时时间必须通过 `VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES` 配置(默认 45 分钟),不得硬编码 | 超时配置无法随 LongJob 场景调整,导致 entry 泄漏或过早清理 | diff --git "a/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" "b/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 00000000000..84ffc2250fa --- /dev/null +++ "b/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,464 @@ +# VM 元数据 — 存储层与模板虚拟机 + +## 目录 + +1. [存储层元数据](#1-存储层元数据) + 1.6. [存储迁移 Poller 暂停的崩溃恢复](#16-存储迁移-poller-暂停的崩溃恢复) +2. [模板虚拟机与链式克隆元数据](#2-模板虚拟机与链式克隆元数据) +3. [新增/修改代码文件清单](#3-新增修改代码文件清单) +4. [约束与不変量](#4-约束与不変量) + +--- + +## 1. 存储层元数据 + +### 1.1 元数据存储路径 + +| 存储类型 | 路径 | 约束 | +|----------|------|------| +| sblk | `/dev/{vg_uuid}/{vm_uuid}_vmmeta` | LV 名 `{vm_uuid}_vmmeta` 长度固定 39(32+7),远小于 LVM 名称上限 128 | +| local/NFS | `{mountPath}/.zstack-vm-metadata/{vm_uuid}.json` | 目录 `.zstack-vm-metadata` 必须以 `0700` 创建 | +| ceph/zbs/vhost | 当前版本不支持,后续按需扩展 | 不创建元数据容器 | + +### 1.2 各存储类型实现 + +**sblk(共享块存储)** + +详见 [Part 4a: sblk 概述](vm-metadata-04a-sblk存储协议概述.md) 及其子文档。核心要点: +- LV 命名:`{vm_uuid}_vmmeta` +- 长度安全性:`vm_uuid`(32 字符)+ `_vmmeta`(7 字符)= 39,低于 LVM 128 字符限制,无截断风险 +- 二进制格式:Header(4KB) + Slot A + Slot B +- 三阶段原子写入 +- 初始大小 4MB,阶梯式扩容至最大 64MB +- LV 初始化时写入 Header 并将完整 payload 写入 Slot A + +**local/NFS** + +- **`mountPath` 定义**:`mountPath = PrimaryStorageVO.url`。PS 所挂载集群的每台 Host 均有此挂载路径。 +- **NFS 前置条件**:NFS PS 的挂载选项已强制 `no_root_squash`(ZStack 创建 NFS PS 时校验并要求),因此 Agent 进程以 root 身份操作元数据文件无权限问题。 +- **目录创建**:`.zstack-vm-metadata` 目录的创建采用与 rootVolume 目录相同的逻辑(权限设置 `0700`,owner=root, group=root),防止非特权用户读取跨 VM 元数据文件。NFS 场景下目录自然跨 Host 共享;local 场景下各 Host 独立创建。 +- **文件路径**:`{mountPath}/.zstack-vm-metadata/{vm_uuid}.json`(集中式目录,便于扫描)。元数据文件跟随根盘所在 PS,即元数据锚定在根盘位置。 +- **初始文件**:`initializeMetadata` 创建元数据文件并写入当前 VM 的完整元数据 payload。若文件不存在则先创建再写入,若已存在则覆盖。创建流程同样使用 tmp + fsync + rename 原子写入路径。 +- **writeMetadata 容器自动创建(讨论 Δ-4)**:`writeMetadata` 执行前自动检查 `.zstack-vm-metadata/` 目录是否存在,不存在时自动创建(`mkdir -p` + `chmod 0700`)。此设计将 `initializeMetadata` 和 `writeMetadata` 的容器创建逻辑统一,无需调用方显式区分"首次写入"和"后续更新"。`initializeMetadata` 在语义上仍然保留(VM 创建场景的入口),但底层实现可直接复用 `writeMetadata` 路径。 +- **文件内容**:DTO JSON 明文(systemTags/resourceConfigs 为 per-Resource Base64 编码) +- **原子写入**:先写 tmp 文件 → `fsync(fd)` 刷盘 → `os.rename()` 替换 → `fsync(dirfd)` 刷新父目录元数据。`os.rename()` 等价于 Linux `mv`,原子替换目标文件,**rename 成功后 tmp 文件不会残留**(tmp 已变为目标文件)。仅在 write-tmp 完成后、rename 之前崩溃时,会残留一个 tmp 文件。`fsync(dirfd)` 保证 NFS 场景下目录项更新对其他客户端可见。并发安全性由 Poller CAS 认领机制保证(同一时刻只有一个 MN 持有某 VM 的 flush 权限),无需额外文件锁。 +- **tmp 文件命名**: + - 常规写入:`{vm_uuid}.json.tmp` + - 存储结构变更写入(`storageStructureChange=true`):`{vm_uuid}.json.sc.tmp` + - 使用固定命名(非随机),每次写入覆盖同名 tmp 文件,避免崩溃后积累多个残留文件。 +- **tmp 文件崩溃清理**:`os.rename()` 成功后 tmp 文件即消失(已成为目标文件),正常运行无残留。仅在崩溃窗口(write-tmp 完成 → rename 之前)会残留一个 `.tmp` 或 `.sc.tmp` 文件。Agent 启动时扫描 `.zstack-vm-metadata/` 目录中的 `*.tmp` 文件并删除即可。 +- **写入前 tmp 清理(讨论补充)**:`writeMetadata` 在写入新 tmp 文件前,先删除同名的旧 tmp 文件(若存在)。使用 `os.O_CREAT | os.O_TRUNC` 标志打开 tmp 文件天然实现覆盖,无需显式删除。此设计避免崩溃后 Agent 未重启时旧 tmp 残留影响后续写入。 +- **`storageStructureChange` 参数**:当 `storageStructureChange=true` 时,使用 `.sc.tmp` 后缀的 tmp 文件。此区分用于**注册时判断元数据是否可用**:若扫描到 `.sc.tmp` 残留,说明存储迁移写入未完成,该元数据文件的内容可能是迁移前的旧版本,注册时需标记为不可靠。写入逻辑本身(fsync + rename)无差异。 +- **完整性校验**:不设 checksum 字段。`rename` 是 POSIX 原子操作,不存在半写文件场景;JSON 解析成功即内容完整。读取时若 `json.loads()` 抛异常 → 视为损坏 → 日志告警 → `markDirty()` → 下轮 Poller 从 DB 全量重建 + +#### local/NFS 各操作异常分析 + +**writeMetadata — 原子写入各阶段异常** + +写入流程:`open(tmp)` → `write(payload)` → `fsync(fd)` → `close(fd)` → `os.rename(tmp, target)` → `open(dirfd)` → `fsync(dirfd)` → `close(dirfd)` + +| 阶段 | 异常类型 | 文件系统状态 | 处理方式 | +|------|---------|-------------|---------| +| **open(tmp) 失败** | `IOError`(磁盘满、权限、目录不存在) | 无 tmp 文件产生,`.json` 不受影响 | Agent 返回错误 → Poller 标记失败 → 指数退避重试 | +| **write(payload) 失败** | `IOError`(磁盘满、NFS 超时) | tmp 文件可能含部分数据,`.json` 不受影响 | `finally` 中 `close(fd)` + 尝试 `os.remove(tmp)`;Agent 返回错误 | +| **write 成功,fsync(fd) 失败** | `IOError`(NFS server 拒绝刷盘) | tmp 数据可能仅在 client 缓存中,`.json` 不受影响 | 同上:`close` + `remove(tmp)` + 返回错误 | +| **fsync 成功,rename 前 Agent 崩溃** | 进程崩溃/OOM/kill | tmp 文件完整残留在磁盘上,`.json` 为上次成功写入的版本 | Agent 重启时扫描 `*.tmp` 删除。Poller 下轮重试 | +| **os.rename(tmp, target) 失败** | `OSError`(极罕见:跨文件系统 rename、NFS stale handle) | tmp 完整存在,`.json` 为旧版本 | 尝试 `os.remove(tmp)` 清理;Agent 返回错误 → 重试 | +| **rename 成功,fsync(dirfd) 前 Agent 崩溃** | 进程崩溃 | local:数据已持久化(ext4/xfs rename 同步更新目录项)。NFS:目录项更新可能未刷到 server,但 NFS client 重连后会自动同步 | 无需特殊处理。NFS 最坏场景:其他 Host 短暂看到旧文件名 → 下轮 Poller 写入时 fsync(dirfd) 补齐 | +| **fsync(dirfd) 失败** | `IOError`(NFS server 异常) | `.json` 内容已正确(rename 已完成),仅目录元数据未保证刷到 server | Agent 日志告警但**视为成功返回**(数据完整性已由 rename 保证,dirfd fsync 仅影响跨客户端可见性延迟) | + +**关键不变量**:在写入流程的任何阶段崩溃或出错,`.json` 文件要么是上一次成功写入的完整版本,要么是本次新写入的完整版本。**不存在读到半写内容的可能**。 + +**writeMetadata — 异常处理伪代码** + +```python +def write_metadata(meta_dir, vm_uuid, payload, storage_structure_change): + target = os.path.join(meta_dir, f"{vm_uuid}.json") + suffix = ".sc.tmp" if storage_structure_change else ".tmp" + tmp = os.path.join(meta_dir, f"{vm_uuid}.json{suffix}") + + fd = None + try: + fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + os.write(fd, payload.encode('utf-8')) + os.fsync(fd) + os.close(fd) + fd = None + + os.rename(tmp, target) # 原子替换,此后 tmp 不再存在 + + # fsync(dirfd) — best-effort,失败不影响数据正确性 + try: + dirfd = os.open(meta_dir, os.O_RDONLY) + os.fsync(dirfd) + os.close(dirfd) + except OSError: + logger.warn(f"fsync(dirfd) failed for {meta_dir}, " + "NFS cross-client visibility may be delayed") + except Exception as e: + if fd is not None: + os.close(fd) + # 清理残留 tmp(rename 前失败时 tmp 可能存在) + try: + os.remove(tmp) + except FileNotFoundError: + pass # rename 已成功或 tmp 未创建 + raise e # 向上层返回错误 +``` + +**initializeMetadata — 异常分析** + +| 阶段 | 异常类型 | 处理方式 | +|------|---------|---------| +| `mkdir(.zstack-vm-metadata)` 失败 | 磁盘满、权限 | Agent 返回错误 → 控制面标记 VM 元数据初始化失败 → `markDirty` 后由 Poller 重试 | +| `mkdir` 成功但 `chmod 0700` 失败 | NFS 权限异常 | 已创建目录可能权限不正确 → Agent 返回错误。下次重试时 `mkdir(exist_ok=True)` + 重新 `chmod` | +| 写入 payload 失败 | 同 writeMetadata 各阶段异常 | 同 writeMetadata 处理。目录已创建但文件不存在 → 下次 `initializeMetadata` 或 Poller `writeMetadata` 时创建 | + +`initializeMetadata` 使用与 `writeMetadata` 相同的 tmp+rename 原子路径,因此文件层面的异常处理完全一致。额外关注点仅在目录创建阶段。 + +**readMetadata — 异常分析** + +| 异常类型 | 处理方式 | +|---------|---------| +| 文件不存在(`FileNotFoundError`) | 返回 `null` → 调用方判断:可能是 VM 新创建尚未初始化,或文件被误删 | +| 文件存在但读取失败(`IOError`) | 返回错误 → 调用方按失败处理(重试或告警) | +| 文件内容非法 JSON(`json.loads()` 异常) | 视为损坏 → 返回错误 → 控制面 `markDirty()` → 下轮 Poller 从 DB 全量重建覆盖写入 | +| NFS stale file handle | Agent 返回错误 → Poller 重试(NFS 重连后恢复) | +| `.sc.tmp` 残留文件检测(讨论补充) | `readMetadata` 读取 `.json` 文件时,同步检查同目录是否存在 `{vm_uuid}.json.sc.tmp` 文件。若存在,说明存储迁移写入未完成(write-tmp 成功但 rename 前崩溃),在返回结果中标记 `storageChangeIncomplete=true`,注册端据此拒绝注册(readStatus = `STORAGE_CHANGE_INCOMPLETE`)。普通 `.tmp` 残留不影响 readStatus(仅代表普通写入中断,`.json` 文件本身仍为上次成功写入的完整版本) | + +**deleteMetadata — 异常分析** + +| 异常类型 | 处理方式 | +|---------|---------| +| 文件不存在(`FileNotFoundError`) | **视为成功**(C-01C-9 幂等约束) | +| 删除失败(`IOError`/权限) | Agent 返回错误 → 控制面同步重试(3 次指数退避:30s/60s/120s)→ 仍失败则残留为孤儿文件,由 Part 2b §8.3 巡检清理。注:VM 删除后 FK CASCADE 已清除 dirty 行,Poller 无法介入 | +| 同时删除 `.tmp`/`.sc.tmp` 残留 | `deleteMetadata` 除了删除 `.json` 文件外,还应尝试删除同名的 `.json.tmp` 和 `.json.sc.tmp`(如存在),避免孤儿 tmp 残留。删除 tmp 失败不影响主操作成功 | + +**NFS 特有异常场景** + +| 场景 | 表现 | 影响 | 处理 | +|------|------|------|------| +| **NFS server 宕机** | 所有文件操作阻塞/超时返回 `EIO` | Poller flush 全部失败 | 指数退避重试。NFS 恢复后自动恢复正常。不影响 VM 运行 | +| **NFS client 端缓存过期** | `readMetadata` 可能读到旧版本 | 扫描/注册场景可能看到过期数据 | 可接受:注册场景会做额外校验;Poller 下轮 flush 覆盖 | +| **NFS mount 断开(`ESTALE`)** | 文件操作返回 `errno=116 ESTALE` | 同 NFS server 宕机 | 同上。Agent 应捕获 `ESTALE` 并返回可重试错误码 | +| **多 Host 并发操作同一 `.json`** | 理论上不会发生(Poller CAS 保证单 MN 持有) | — | 防御性措施:若检测到文件被意外修改(mtime 变化),日志告警但不中断写入 | + +### 1.3 MetadataStorageHandler 接口 + +不同存储类型的元数据读写操作通过统一接口抽象: + +```java +public interface MetadataStorageHandler { + void initializeMetadata(String psUuid, String vmUuid, String payloadJson, Completion completion); + void deleteMetadata(String psUuid, String vmUuid, Completion completion); + void writeMetadata(String psUuid, String vmUuid, String payloadJson, + boolean storageStructureChange, Completion completion); + void readMetadata(String psUuid, String vmUuid, ReturnValueCompletion completion); + boolean isMetadataSupported(String psType); + + /** + * 扫描指定 PS 上所有元数据条目,返回 VmMetadataEntry 列表(轻量级,不读取 payload)。 + * sblk: 扫描 VG 中所有 *_vmmeta LV,提取 vmUuid 前缀 + * local/NFS: 列举 .zstack-vm-metadata/ 目录下 *.json 文件名 + * 用途: MetadataOrphanDetector (Part 2b §8.4.2)、Scan API (Part 5 §2) + * + * 返回类型变更说明(讨论 Δ-7):原方案返回 List(纯 vmUuid), + * 改为返回 List,其中 VmMetadataEntry 包含: + * - vmUuid: String — 虚拟机 UUID + * - hostUuid: String — 对于 Local Storage,标识元数据文件所在 Host; + * 对于 SharedBlock/NFS 等共享存储,hostUuid 可为 null。 + * 原因:Local Storage 场景下扫描需要逐 Host 执行,调用方需要知道元数据 + * 位于哪台 Host 上以便后续操作(如孤儿清理、注册时路由)。若仅返回 vmUuid, + * 调用方无法区分同一 PS 不同 Host 上的元数据条目。 + */ + void scanMetadataVmUuids(String psUuid, ReturnValueCompletion> completion); + + /** + * 元数据扫描结果条目。 + */ + class VmMetadataEntry { + private String vmUuid; + private String hostUuid; // nullable: SharedBlock/NFS 场景为 null + } +} +``` + +**重要设计约束**:Agent 端不解析 DTO 内容。控制面负责 DTO 的构建、序列化和反序列化。Agent 只负责将 payload 原样写入/读取。 + +| 实现类 | 存储类型 | initializeMetadata | writeMetadata | readMetadata | deleteMetadata | +|--------|---------|-------------------|---------------|--------------|----------------| +| `SblkMetadataStorageHandler` | SharedBlock | 创建 LV + 写 Header + 写入完整 payload | 三阶段原子写入 LV | 读 Header + Active Slot | `lv_delete` | +| `LocalNfsMetadataStorageHandler` | Local/NFS | 创建文件 + 写入完整 payload | tmp(区分 `.tmp`/`.sc.tmp`)+ fsync + rename | 读 JSON(解析失败视为损坏) | `os.remove()` | + +**Handler 动态路由**(SM-07 修复):`MetadataStorageHandler` 通过 `psUuid` 参数动态路由——每次调用时根据 `PrimaryStorageVO.type` 查找对应 Handler 实现,支持同一迁移流程中源/目标使用不同 Handler。例如 VM 从 SharedBlock 迁移到 NFS 时,Step 4 `initializeMetadata(targetPsUuid)` 路由到 `LocalNfsMetadataStorageHandler`,Step 7 `deleteMetadata(sourcePsUuid)` 路由到 `SblkMetadataStorageHandler`。 + +### 1.4 元数据生命周期 + +| 事件 | 行为 | +|------|------| +| 新创建虚拟机 | 自动创建元数据文件 | + +**VM 创建失败时的元数据清理**:`APICreateVmInstanceMsg` 的 FlowChain 在末尾 Flow 调用 `initializeMetadata` + `markDirty`。若 FlowChain 中更早的 Flow(如分配 IP、创建磁盘)失败,FlowChain 的 rollback 机制会回退所有已完成 Flow(包括 VmInstanceVO 本身通过 `VmAllocateVolumeFlow.rollback` 等清理)。由于 `initializeMetadata` Flow 尚未执行,存储侧不存在元数据文件,无需清理。若 `initializeMetadata` 本身执行成功但后续 Flow 失败(极端场景),`VmCreationRollbackFlow` 应包含 `deleteMetadata` 调用清理残留。若 `initializeMetadata` 执行失败,FlowChain rollback 删除所有已创建 VO,FK CASCADE 清理 dirty 行(如有),孤儿文件由 Part 2b §8.4 巡检兜底。 + +| VM 删除 | 同步删除元数据文件;删除失败时同步重试(3 次指数退避),仍失败 → 孤儿 LV/文件残留 → 由健康巡检([Part 2b §8.3](vm-metadata-02b-高可用与运维.md#83-vm-销毁时的元数据清理))兜底清理。注意:VM 删除后 FK CASCADE 已删除 `VmMetadataDirtyVO` 行,Poller 无法介入,因此使用同步重试 | + +**元数据删除时机(讨论 Δ-5)**:元数据文件的删除发生在 **ExpungeVmInstanceFlow**(物理删除阶段),而非 DestroyVmInstanceFlow(软删除阶段)。原因: +1. DestroyVm 仅执行软删除(`VmInstanceVO` → `VmInstanceEO`),VM 可通过 `APIRecoverVmInstanceMsg` 恢复。若在 Destroy 时删除元数据,Recover 后元数据丢失且无法自动恢复(需手动触发全量刷写)。 +2. Expunge 是不可逆的物理清除,此时删除元数据是安全的(VM 不可能再恢复)。 +3. Destroyed 状态的 VM 已被 Poller 的前置检查过滤(Part 2 §4.4),不会执行无效刷写。 +4. Destroy → Expunge 窗口内元数据保留不影响存储空间(元数据文件通常 <500KB)。 + +**deleteMetadata 重试参数可配**:当前硬编码 3 次重试(30s/60s/120s)。改为通过 GlobalConfig 配置:`vm.metadata.delete.maxRetry`(默认 3)、`vm.metadata.delete.baseDelaySec`(默认 30)。计算方式:`baseDelay × 2^(retryIndex)`,与 Poller 退避公式一致。此配置项添加到 Part 2b §13 GlobalConfig 汇总表。 + +| 存储迁移 | 暂停 Poller → 数据迁移 → DB 更新 → 目标端初始化写入与校验 → 恢复 Poller + markDirty → 源端清理 | +| 不支持的存储类型 | 静默跳过,不创建元数据文件 | + +**存储迁移场景分类**(SM-08 修复): + +| 场景 | 条件 | 元数据处理 | +|------|------|-----------| +| **(A) 整 VM 存储迁移** | Root Volume 参与迁移(含或不含 DataVolume) | 执行完整 7-step 流程:暂停 Poller → 数据迁移 → DB 更新 → 目标端初始化写入 + read-back 校验 → 恢复 Poller → 源端清理 | +| **(B) 单 DataVolume 迁移** | 仅 DataVolume 迁移,Root Volume 不动 | 元数据锚定在 Root PS,**无需**暂停 Poller / initializeMetadata / deleteMetadata。迁移完成后仅需 `markDirty(vmUuid, true)` 触发 Poller 重写(因 `VolumeVO.installPath` 已变更,payload 需更新) | + +以下 7-step 流程仅适用于场景 (A)。场景 (B) 的判断依据:迁移的卷列表中不包含 `VolumeVO.type = Root` 的卷。 + +**存储迁移时的元数据生命周期**(强一致路径,失败阻断源端清理): + +``` +Step 1: 暂停该 VM 的 Poller flush + INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) VALUES (:vmUuid, 1, 1) + UPDATE VmMetadataDirtyVO SET nextRetryTime='2099-12-31 23:59:59' WHERE vmInstanceUuid=:vmUuid +``` + +**Step 1 INSERT IGNORE 说明(讨论 Δ-6)**:原方案直接 UPDATE `nextRetryTime`,但若该 VM 当前无 dirty 行(已被 Poller 成功处理后删除),UPDATE 将匹配 0 行,后续 Step 6 恢复 Poller 时也无行可操作。改为先 `INSERT IGNORE`(确保 dirty 行存在)再 `UPDATE`(设定暂停哨兵值),与 `markDirty()` 的两步语义保持一致。`storageStructureChange=1` 因为存储迁移必然涉及存储拓扑变更。 +Step 2: 数据迁移(卷/快照) +Step 3: DB 更新(VolumeVO.primaryStorageUuid/installPath) +Step 4: initializeMetadataOnTargetPS(vmUuid, targetPsUuid) + # DB 已指向目标端,payload 基于最新 DB 构建,installPath 已是目标端路径。 + # 同时完成容器创建(sblk LV / NFS 目录)和正确 payload 写入,无 stale 数据。 + # SM-03 交叉引用:storageStructureChange=true → OP type=2 STORAGE_CHANGE,参见 Part 4c §3 +Step 5: readMetadata(targetPsUuid, vmUuid) + readStatus 校验 + JSON 可解析性验证(参见 D-1c-1,不设 checksum) +Step 6: 恢复 Poller(nextRetryTime=NULL)并 markDirty(vmUuid, true) +Step 7: deleteMetadataOnSourcePS(vmUuid, sourcePsUuid) +``` + +Step 5 为必选保护:在源端清理前,目标端必须已有完整元数据,禁止仅依赖异步 Poller 首刷。 + +**为什么 initializeMetadata 在 DB 更新之后执行**:在 §1.4 流程中,`initializeMetadataOnTargetPS` 被安排在 Step 4(DB 更新之后),而非数据迁移之前。这一设计消除了 stale payload 问题——payload 基于最新 DB 构建,`installPath` 已指向目标端,无需额外覆盖写入。若采用旧方案(在数据迁移前预创建元数据),payload 中的 `installPath` 仍指向源 PS,需要后续步骤覆盖,且在 MN 崩溃时会产生内容过期的孤儿元数据。当前方案的优势:(1) 一次写入即正确,无 stale 数据;(2) 缩小孤儿窗口——仅 Step 4 成功后、Step 5~6 失败时才需要 SM-01 回滚清理目标端残留;(3) sblk 场景下无需先创建空 LV 再覆盖。 + +**Step 6 `markDirty` 防御性设计理由**(SM-05 修复):Step 4 已同步写入完整元数据到目标端,Step 6 的 `markDirty(vmUuid, true)` 看似冗余,但作为防御性措施是必要的——Step 2~5 执行期间可能有其他 API 修改 VM 配置(如热插拔网卡、修改 HA 级别),导致 Step 4 同步写入的内容与最新 DB 状态存在微小时间差。`markDirty` 确保 Poller 异步刷写能将目标端元数据收敛到最终一致状态。 + +**迁移清理前 double-check(Root Volume)**: + +```java +String rootPsUuid = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .eq(VolumeVO_.type, VolumeType.Root) + .select(VolumeVO_.primaryStorageUuid) + .findValue(); +if (oldPsUuid.equals(rootPsUuid)) { + logger.warn("VM {} root volume still on source PS {}, skip metadata cleanup", vmUuid, oldPsUuid); + return; +} +String targetPayload = metadataStorageHandler.readMetadata(targetPsUuid, vmUuid); +if (targetPayload == null || targetPayload.trim().length() <= 2) { // 防御性检查:异常场景下可能出现空文件或损坏文件 + logger.warn("VM {} target metadata is empty, skip source cleanup", vmUuid); + return; +} +metadataStorageHandler.deleteMetadata(oldPsUuid, vmUuid, ...); +``` + +元数据锚定在根盘所在 PS,因此 double-check **仅需校验根盘**的 `primaryStorageUuid` 是否仍在源 PS 上。DataVolume 的位置不影响元数据存储位置。 + +**C-01C-9**(约束):`deleteMetadata` 必须幂等——删除不存在的元数据(LV 已删除或 JSON 文件不存在)必须返回成功(不抛异常)。`SblkMetadataStorageHandler.deleteMetadata` 中 `lv_delete` 对不存在的 LV 应返回 0(非错误);`LocalNfsMetadataStorageHandler.deleteMetadata` 中 `os.remove()` 应捕获 `FileNotFoundError` 并视为成功。 + +**失败回滚策略**:Step 2-6 任一步失败,必须执行 `nextRetryTime=NULL` 恢复 Poller,且不得执行 Step 7。后续通过 `markDirty(vmUuid, false)` 回到源路径刷写。 + +**SM-01 修复:Step 4 成功后的目标端清理**:若 Step 4 `initializeMetadataOnTargetPS` 已成功(目标端 LV 或 JSON 文件已创建并写入 payload),Step 5~6 任一步失败时,回滚必须先执行 `deleteMetadata(targetPsUuid, vmUuid)` 清理目标端残留,再恢复 Poller。不清理会导致:(1) 后续重试时 `initializeMetadata` 报"已存在"错误;(2) 目标端残留成为孤儿资源。回滚顺序:`deleteMetadata(target)` → `nextRetryTime=NULL` → `markDirty(vmUuid, false)`。 + +**flush 路径解析策略**(关联 [Part 2b §8.2](vm-metadata-02b-高可用与运维.md#82-路径指纹巡检--轻量级漂移检测)): + +- `doFlush()` 每次都从当前 `VolumeVO.installPath/primaryStorageUuid` 动态解析目标 PS +- 禁止缓存上一次 flush 的 psUuid/path +- 迁移回滚后下一轮 flush 自动回到源 PS(对应 Q2b-7 修复点) + +**存储迁移时序分析**(QX-1 全链路一致性): + +``` +T1: pause poller(nextRetryTime=FAR_FUTURE) — 对应 Step 1 +T2: 数据搬迁(卷/快照) — 对应 Step 2 +T3: DB installPath/psUuid 切换为 target — 对应 Step 3 +T4: initializeMetadataOnTargetPS(vmUuid, targetPsUuid) — 对应 Step 4 + # DB 已指向目标端,payload 基于最新 DB 构建,installPath 已是目标端路径。 + # 同时完成容器创建(sblk LV / NFS 目录)和正确 payload 写入,无 stale 数据。 + # SM-02 崩溃窗口:T4→T5 之间若 MN 崩溃,目标端已创建元数据但尚未校验。 + # DB 已指向目标端,§1.6 崩溃恢复重置 nextRetryTime 后,Poller 基于当前 DB + # 写入目标端,自动收敛到正确状态。无孤儿风险(DB 与元数据位置一致)。 +T5: readMetadata + read-back verify + JSON 可解析性验证 — 对应 Step 5 +T6: resume poller(nextRetryTime=NULL) + markDirty(vmUuid, true) — 对应 Step 6 +T7: root/data volume 双重校验 + target 非空校验 → deleteMetadata — 对应 Step 7 +``` + +**关键保证**:源端清理前已经完成目标端同步写入和 read-back 校验;Poller 仅作为后续收敛机制,而非迁移正确性的前置条件。 + +**与旧方案的差异**:早期设计中 `initializeMetadataOnTargetPS` 在数据迁移之前执行(预创建),此时 DB 尚未切换,写入的 payload 包含源端 `installPath`(stale 数据),需后续覆盖。现已调整为 Step 4(DB 更新之后),消除 stale payload 问题,并缩小了 SM-02 崩溃窗口的影响——崩溃后 Poller 恢复即可写入正确的目标端数据,无需孤儿巡检兜底。 + +### 1.5 不支持的存储类型 + +| 场景 | 行为 | +|------|------| +| VM 根盘在不支持的存储上 | 静默跳过,不创建元数据文件 | +| `@MetadataImpact` 拦截器触发时 | 检查根盘存储类型,不支持的直接跳过 markDirty | +| 注册 API 指定不支持的存储 | 返回错误 `METADATA_STORAGE_NOT_SUPPORTED` | + +**Local Storage + VM 热迁移(非存储迁移)的元数据处理**:VM 热迁移(`APIMigrateVmMsg`)仅迁移 VM 进程,不移动磁盘数据。Local Storage 场景下,VM 热迁移**不支持**(ZStack 约束:Local Storage 的 VM 不允许热迁移,仅允许存储迁移)。因此不存在"VM 迁移到另一 Host 但元数据文件在源 Host Local 磁盘上"的场景。SharedBlock/NFS 场景下热迁移不影响元数据位置(通过共享存储访问)。此场景无需额外处理。 + +### 1.6 存储迁移 Poller 暂停的崩溃恢复(H3 修复) + +**问题**:§1.4 Step 1 将 `nextRetryTime` 设为 `'2099-12-31 23:59:59'` 暂停 Poller,Step 6 恢复为 NULL。若 MN 在 Step 1 之后、Step 6 之前崩溃(或迁移流程异常退出未触发失败回滚),该 dirty 行的 `nextRetryTime` 将永久停留在远未来值,导致该 VM 的元数据刷写被永久阻塞。 + +**修复方案 — MN 启动扫描 + 自动重置**: + +在 `managementNodeReady()` 回调中,Poller 启动前执行一次性扫描: + +```java +/** + * 崩溃恢复:检测并重置被迁移暂停但未恢复的 dirty 行。 + * 判断条件:nextRetryTime > NOW() + 1 hour(正常退避最大值远小于此) + * 安全性:若迁移确实仍在进行(MN 未崩溃),该行的 managementNodeUuid 不为 NULL, + * 不会被 Poller 认领;此处仅重置 nextRetryTime 和 retryCount,不修改认领状态。 + */ +private void recoverStalledMigrationPauses() { + // DP-10 修复:改为精确匹配迁移暂停哨兵值 '2099-12-31 23:59:59', + // 避免误重置正常指数退避的行(最大退避约 2.8h,远小于此阈值)。 + // 原代码使用 `> TIMESTAMPADD(HOUR, 1, CURRENT_TIMESTAMP)` 存在误重置风险。 + int recovered = SQL.New( + "UPDATE VmMetadataDirtyVO " + + "SET nextRetryTime = NULL, retryCount = 0 " + // SM-09 修复:同时重置 retryCount,给予完整重试配额 + "WHERE nextRetryTime = '2099-12-31 23:59:59'") + .execute(); + if (recovered > 0) { + logger.warn("Recovered {} dirty rows with stalled migration pause (nextRetryTime far in future)", recovered); + } +} +``` + +**调用时机**:`managementNodeReady()` 中,先调用 `recoverStalledMigrationPauses()`,再启动 Poller(`thdf.submitPeriodicTask()`)。 + +**安全性分析**: + +| 场景 | 行为 | 安全性 | +|------|------|--------| +| MN 崩溃后重启,迁移已失败 | `nextRetryTime` 被重置为 NULL → Poller 正常认领 → 从 DB 全量重建 | (Y) 安全(DB 已反映回滚后状态) | +| MN 崩溃后重启,迁移已成功(Step 4 DB 已切换) | 同上,flush 到新 PS | (Y) 安全(DB installPath 已指向目标) | +| 双 MN 场景,另一 MN 正在执行迁移 | dirty 行 `managementNodeUuid` 不为 NULL → Poller CAS 条件排除 → 不会重复处理 | (Y) 安全(仅重置时间,不抢认领) | +| 正常退避中的 dirty 行(retryCount < max) | 退避最大值 = `baseDelay × 2^maxExponent`(默认 10 × 1024 ≈ 10240s ≈ 2.8h) | (Y) 安全(精确匹配哨兵值,不会误重置正常退避行) | + +**已采用精确匹配**(DP-10 修复):使用 `nextRetryTime = '2099-12-31 23:59:59'` 而非 `> NOW() + 1h`,完全消除误重置正常退避行的风险。最终 SQL: + +```sql +UPDATE VmMetadataDirtyVO SET nextRetryTime = NULL, retryCount = 0 +WHERE nextRetryTime = '2099-12-31 23:59:59' +``` + +**与 §1.4 失败回滚的关系**:§1.4 的失败回滚策略("Step 2-6 任一步失败,必须执行 `nextRetryTime=NULL` 恢复 Poller")覆盖了**正常失败**场景。本节 H3 修复覆盖的是**异常退出**场景(MN 崩溃、JVM OOM、进程被 kill 等导致回滚逻辑未执行)。两者互补,无冲突。 + +--- + +## 2. 模板虚拟机与链式克隆元数据 + +### 2.1 模板 VM 数据模型概述 + +``` +VmInstanceVO (type = "UserVm", 模板 VM) + │ uuid (1:1, CASCADE) + ▼ +TemplatedVmInstanceVO ← 纯标记表 + ├── TemplatedVmInstanceCacheVO ← 缓存 VM + │ └── cacheVmInstanceUuid → VmInstanceVO (缓存 VM) + │ └── VolumeVO → VolumeSnapshotVO + │ └── VolumeSnapshotReferenceVO ← 子 VM 的引用记录 + └── TemplatedVmInstanceRefVO ← 子 VM 追溯 + └── vmInstanceUuid → VmInstanceVO (子 VM) +``` + +### 2.2 元数据策略 + +#### 模板 VM(vmCategory = TEMPLATE) + +写入元数据,注册时作为普通 VM 恢复(不恢复模板身份)。 + +模板 VM 的元数据存储位置与普通 VM 一致:**以 RootVolume 所在 Primary Storage 为唯一存储锚点**,不使用 `TemplatedVmInstanceCacheVO` 的缓存卷位置作为元数据路径来源。 + +**不纳入元数据的关联表**: + +| VO | 理由 | +|----|------| +| `TemplatedVmInstanceVO` | 纯标记表无业务字段,`vmCategory=TEMPLATE` 已标记身份 | +| `TemplatedVmInstanceCacheVO` | 缓存 VM 是运行态产物,跨环境无意义 | +| `TemplatedVmInstanceRefVO` | 子 VM 追溯关系属于旧环境 | + +#### 缓存 VM(vmCategory = TEMPLATE_CACHE) + +**写入元数据**(供扫描展示),但**拒绝注册**。 + +- 写入理由:扫描结果中 admin 可识别缓存 VM 身份 +- 不注册理由:缓存 VM 是内部运行态资源,新环境自动创建 + +#### 子 VM / 链式克隆(vmCategory = REGULAR) + +作为普通 VM 注册,额外恢复 `VolumeSnapshotReferenceTreeVO` 和 `VolumeSnapshotReferenceVO`。注册后等效于**模板和缓存已被删除**的状态。 + +### 2.3 VolumeSnapshotReferenceVO/TreeVO 的 FK 约束分析 + +权威 FK 定义见 [Part 1a §2.4](vm-metadata-01a-数据模型与序列化.md#24-volumeresourcemetadata)。 + +**DDL 层面 FK 约束摘要**: + +| 表 | 字段 | FK 目标 | ON DELETE | 注册安全性 | +|----|------|---------|-----------|------------| +| `VolumeSnapshotReferenceTreeVO` | 所有字段 | **无 FK 约束** | — | (Y) 可直接插入,`rootVolumeSnapshotUuid`/`rootVolumeUuid`/`hostUuid` 等均为逻辑引用 | +| `VolumeSnapshotReferenceVO` | `referenceVolumeUuid` | `VolumeEO.uuid` | CASCADE | (Y) 指向子 VM 的卷,注册时先创建 VolumeVO 即可满足 | +| `VolumeSnapshotReferenceVO` | `treeUuid` | `VolumeSnapshotReferenceTreeVO.uuid` | SET NULL | (Y) 先插入 TreeVO 即可满足 | +| `VolumeSnapshotReferenceVO` | `parentId` | 自引用 `VolumeSnapshotReferenceVO.id` | SET NULL | (Y) 按层级顺序插入 | +| `VolumeSnapshotReferenceVO` | `volumeUuid`, `volumeSnapshotUuid`, `directSnapshotUuid`, `referenceUuid` | **无 FK 约束** | — | (Y) 可引用旧环境不存在的 UUID(逻辑引用) | + +**关键结论**:`VolumeSnapshotReferenceTreeVO.rootVolumeSnapshotUuid` 无 FK 到 `VolumeSnapshotVO`,因此注册子 VM 时即使缓存 VM 的快照不存在于新环境,TreeVO 插入也不会违反约束。注册顺序:`VolumeVO`(子 VM 卷)→ `VolumeSnapshotReferenceTreeVO` → `VolumeSnapshotReferenceVO`。 + +子 VM 的 Reference 记录在缓存 VM 被删除后仍然安全可用。代码层面验证(`VolumeSnapshotReferenceUtils.java`): + +| 操作场景 | 是否需要缓存 VM 的 VolumeSnapshotVO | 原因 | +|---------|:---:|------| +| 删除子 VM 卷 | 否 | `backingVolumeDeletedInDb=true` 时直接走 `deleteBitsOnPs` | +| Flatten 子 VM(无快照) | 否 | `referenceType=VolumeVO` → 直接删 ref | +| Flatten 子 VM(有快照) | 仅子 VM 自己的 | `ref.getReferenceUuid()` 查子 VM 快照 | +| 子 VM 创建快照 | 否 | 仅更新 ref 字段 | +| 子 VM 删除快照 | 仅子 VM 自己的 | 查询条件限制为子 VM 快照 UUID | +| 子 VM 卷路径变更 | 否 | 仅更新 `referenceInstallUrl` | + +### 2.4 模板相关 API 的 @MetadataImpact + +| 操作 | @MetadataImpact | vmCategory 变化 | +|------|----------------|-----------------| +| 普通 VM 转模板 VM | `CONFIG` | REGULAR → TEMPLATE | +| 模板 VM 转回普通 VM | `CONFIG` | TEMPLATE → REGULAR | +| 从模板创建子 VM(首次) | 不影响模板本身 | 自动创建 TEMPLATE_CACHE | +| 更新模板 VM 属性 | `CONFIG` | 不变 | + +--- + +## 3. 约束与不変量 + +| 约束 ID | 内容 | 来源章节 | +|---------|------|----------| +| C-01C-2 | sblk LV 名称使用 `{vm_uuid}_vmmeta`,长度计算为 39,必须始终小于 LVM 128 上限 | §1.1, §1.2 | +| C-01C-3 | 模板 VM(TEMPLATE)元数据写入位置锚定 RootVolume 所在 PS,不依赖 cache VM 路径 | §2.2 | +| C-01C-4 | 存储迁移必须在源端清理前完成目标端同步写入与 read-back 校验;禁止仅依赖异步 Poller 首次刷写 | §1.4 | +| C-01C-5 | 存储迁移清理必须校验根盘的 `primaryStorageUuid` 是否仍在源 PS,若根盘仍在源 PS 则不得 deleteMetadata(source) | §1.4 | +| C-01C-6 | flush 路径必须按 `VolumeVO.installPath/primaryStorageUuid` 动态解析,不得缓存历史路径 | §1.4 | +| C-01C-7 | 迁移期间对 dirty 行 `nextRetryTime` 的暂停/恢复必须成对出现;失败回滚时必须恢复 Poller | §1.4 | +| C-01C-8 | MN 启动时必须扫描并重置 `nextRetryTime='2099-12-31 23:59:59'` 的迁移暂停行;该扫描必须在 Poller 启动之前执行 | §1.6 | +| C-01C-9 | `deleteMetadata` 必须幂等——删除不存在的元数据必须返回成功(不抛异常) | §1.4 | +| C-01C-10 | local/NFS 的 tmp 文件使用固定命名(`.tmp`/`.sc.tmp`),Agent 启动时扫描清理 `*.tmp` 残留 | §1.2 | +| C-01C-11 | `MetadataStorageHandler` 接口必须包含 `scanMetadataVmUuids()` 方法,用于孤儿检测和 Scan API | §1.3 | +| C-01C-12 | `deleteMetadata` 重试参数(次数、退避基础延迟)必须通过 GlobalConfig 配置,不得硬编码 | §1.4 | diff --git "a/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" "b/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" new file mode 100644 index 00000000000..dfcb00afd92 --- /dev/null +++ "b/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" @@ -0,0 +1,939 @@ +# VM 元数据 — 脏标记与 Poller + +## 目录 + +1. [概述](#1-概述) +2. [数据模型](#2-数据模型) +3. [markDirty — 标脏入口](#3-markdirty--标脏入口) +4. [MetadataDirtyPoller — 轮询刷写](#4-metadatadirtypoller--轮询刷写) + - [4.8 Stale 恢复任务](#48-stale-恢复任务h2-修复) +5. [消息调用链](#5-消息调用链) +6. [并发控制(四层)](#6-并发控制四层) + - [6.4 调优指南](#64-调优指南) +7. [约束与不変量](#7-约束与不変量) + +--- + +# 1. 概述 + +## 1.1 问题:GC 框架的结构性错配 + +GC 框架是 **"一个任务对应一行 DB 记录"** 的模型。每次 API 成功都 `submit()` 创建新 GC 行,通过 ChainTask `maxPendingTasks=1` + `exceedMaxPendingCallback` 淘汰多余行。这导致: + +| 问题 | 说明 | +|------|------| +| **GC 行爆炸** | 100 个 API → 100 行 GC,98 行立即 Done,需定期清理 | +| **deduplicateSubmit 不可用** | GC 执行期间 status 仍为 Idle,新 GC 被误判"已有在处理" | +| **双 MN 复杂度** | 需 hash 环路由 SubmitGCMsg + 执行层 delegation + reply 回退,6 种极端情况需逐一分析 | +| **delegation 消耗 retryCount** | 非 owner 上 triggerNow 的 delegation 失败也递增 retryCount | +| **框架修改** | 需修改 loadOrphanJobs 增加状态过滤、需新增索引 | + +**根因**:元数据更新需要的是 **"标脏 → 合并 → 刷写"** 模型(多次修改合并为一次写入),而非 GC 的 **"一个失败任务 → 一次重试"** 模型。 + +## 1.2 新方案一句话 + +用一张 **`VmMetadataDirtyVO`** 表做脏标记(一个 VM 最多一行),**`PeriodicTask`** 轮询器定期认领并刷写,成功删行,失败释放等下轮。 + +灵感来源:`SecurityGroupFailureHostVO` + `FailureHostWorker` 模式。 + +## 1.3 核心不变量 + +- 刷写时始终从 DB 查询 VM 完整当前状态构建 payload,不使用触发 API 时的增量数据。 +- 任何一次刷写完成后,存储上的元数据反映数据库最新完整状态。 +- `buildVmInstanceMetadata()` 必须标注 `@Transactional(readOnly = true)`,MySQL InnoDB REPEATABLE READ 事务内所有查询使用同一快照,保证单次构建的读一致性。`readOnly = true` 不启动写事务,开销极小。 + +## 1.4 最终一致性模型 + +`buildVmInstanceMetadata()` 读 DB 到 `pwrite` 完成之间存在毫秒级窗口,期间其他 API 可能修改了 DB(如删除快照)。此时写入的元数据可能包含已过期信息。这不是问题——修改 DB 的 API 成功后会再次 `markDirty()`,下轮 Poller 从 DB 全量读取已反映最新状态,覆盖写入自然修正。 + +对注册场景,Part 3 §3.4 的 installPath 存在性检查提供额外兜底。 + +--- + +# 2. 数据模型 + +## 2.1 VmMetadataDirtyVO + +```java +@Entity +@Table +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; // null = 未认领,非null = 已认领 + + @Column + private Timestamp lastClaimTime; // 最近一次被 CAS 认领的时间(死锁防护) + + @Column + private long dirtyVersion; // 每次 markDirty 递增,用于检测刷写期间的新变更 + + @Column + private boolean storageStructureChange; // 是否涉及存储结构变更(OP type 标记) + + @Column + private int retryCount; // 连续失败次数 + + @Column + private Timestamp nextRetryTime; // 下次可被认领的时间(退避控制) + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; // 最后一次 markDirty 的时间(关键!) +} +``` + +**关键设计决策**: + +| 设计点 | 决策 | 原因 | +|--------|------|------| +| `vmInstanceUuid` 做主键 | 一个 VM 最多一行 | 天然去重,100 个 API 只产生 1 行,不是 100 行 | +| `managementNodeUuid` FK SET_NULL | MN 宕机自动释放 | 无需额外孤儿扫描,DB 约束自动完成 | +| `lastClaimTime` | claim 存活时间上限控制 | 识别僵尸 claim,支持 stale 认领接管 | +| `vmInstanceUuid` FK CASCADE | VM 销毁自动删除脏标记 | 无残留 | +| `dirtyVersion` | 每次 markDirty +1 | 刷写前快照 version,成功后比较——检测刷写期间是否有新变更(见 §4.5)。语义比时间戳比较更明确,无精度问题 | + +**`dirtyVersion` per-row 语义澄清**:`dirtyVersion` 是每行独立的单调递增计数器(从 1 开始),不是全局序列号。其用途仅限于同一 VM 的 `onFlushSuccess` 版本比较(检测刷写期间是否有新 markDirty),不用于跨 VM 排序。跨 VM 公平调度使用 `lastOpDate`。BIGINT 范围(9.2×10^18)足够单 VM 终身使用(假设 1000 次/秒,需 2.9 亿年溢出),无需溢出保护。 +| `storageStructureChange` | OR 升级策略 | `@MetadataImpact(CONFIG)` → false(OP type 1),`@MetadataImpact(STORAGE)` → true(OP type 2)。多次 markDirty 取 OR:一旦标记为 STORAGE 则本轮不降级 | +| `lastOpDate` | MySQL 自动更新 | Poller 认领时排序依据(最早变更优先处理) | +| `nextRetryTime` | 退避控制 | 失败后不立刻重试,等到下次重试时间 | + +## 2.2 DDL + +```sql +CREATE TABLE VmMetadataDirtyVO ( + vmInstanceUuid VARCHAR(32) NOT NULL, + managementNodeUuid VARCHAR(32) DEFAULT NULL, + lastClaimTime TIMESTAMP NULL DEFAULT NULL, + dirtyVersion BIGINT NOT NULL DEFAULT 1, + storageStructureChange TINYINT(1) NOT NULL DEFAULT 0, + retryCount INT NOT NULL DEFAULT 0, + nextRetryTime TIMESTAMP NULL DEFAULT NULL, + createDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + lastOpDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + 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; +``` + +## 2.3 推荐索引 + +```sql +-- Poller CAS 认领查询: WHERE managementNodeUuid IS NULL AND nextRetryTime <= NOW() +CREATE INDEX idx_dirty_unclaimed ON VmMetadataDirtyVO (managementNodeUuid, lastClaimTime, nextRetryTime); +``` + +**约束**:`lastClaimTime` 允许为空(历史数据兼容);新版本 claim 路径必须在 CAS 成功时写入当前时间。 + +与 GarbageCollectorVO 的详细对比见 [对比文档 §1](2/vm-metadata-new-02h-compare.md#1-数据模型对比vmmetadatadirtyvo-vs-garbagecollectorvo)。 + +--- + +# 3. markDirty — 标脏入口 + +## 3.1 核心逻辑 + +```java +public boolean markDirty(String vmInstanceUuid, boolean storageStructureChange) { + // Q23 修复:返回 boolean 表示标脏是否成功(供 MetadataStaleRecoveryTask DP-03 使用) + // 前置检查:功能开关 + if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { + return false; + } + + // Q2-2: Galera 集群兼容写法,避免 INSERT ON DUPLICATE KEY 在高并发下死锁 + // Step 1: INSERT IGNORE(新行) + int inserted = SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "VALUES (:vmUuid, 1, :ssc)") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + + // Step 2: 仅在行已存在时执行 UPDATE(dirtyVersion +1, storageStructureChange OR 升级) + if (inserted == 0) { + int updated = SQL.New("UPDATE VmMetadataDirtyVO " + + "SET dirtyVersion = dirtyVersion + 1, " + + " storageStructureChange = storageStructureChange OR :ssc " + + "WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + + // Q19 修复:INSERT IGNORE 返回 0(行已存在)但 UPDATE 也返回 0(行被并发删除) + // 竞态窗口:INSERT IGNORE → onFlushSuccess DELETE → UPDATE(行已不存在) + // 此时必须重新 INSERT,否则本次 markDirty 对应的 DB 变更将丢失 + if (updated == 0) { + SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "VALUES (:vmUuid, 1, :ssc)") + .param("vmUuid", vmInstanceUuid) + .param("ssc", storageStructureChange) + .execute(); + } + } + + // 立即唤醒:尝试认领并提交刷写,不等待 Poller 轮询 + triggerFlushForVm(vmInstanceUuid); + return true; + } catch (Exception e) { + logger.warn("markDirty failed for vm={}: {}", vmInstanceUuid, e.getMessage()); + return false; + } +} +``` + +### 竞态分析与修复 + +**问题**:`INSERT IGNORE` 与 `UPDATE` 是两个非原子操作,存在以下竞态窗口: + +``` +T1: API-A 修改 DB +T2: API-A 调用 markDirty → INSERT IGNORE → inserted=0(行已存在,dirtyVersion=N) +T3: Poller flush 完成 → onFlushSuccess: DELETE WHERE dirtyVersion=N → 行被删除 +T4: API-A: UPDATE WHERE vmInstanceUuid=:vmUuid → updated=0(行已不存在) +T5: API-A 调用 triggerFlushForVm → 无 dirty 行 → skip +→ API-A 的 DB 变更未被 flush 刷写! +``` + +**关键**:T3 的 flush 读 DB 快照在 T1 之前(flush 早于 API-A 的 DB 变更),因此写入的元数据不包含 API-A 的修改。T3 DELETE 成功因为 `dirtyVersion` 未被递增(T4 还未执行)。 + +**修复**:当 `inserted == 0 && updated == 0` 时,重新执行 `INSERT IGNORE`。使用 `INSERT IGNORE` 而非 `INSERT` 保证并发安全(若另一线程同时插入,IGNORE 避免异常)。重新创建的行 `dirtyVersion=1`,Poller/triggerFlush 将从 DB 全量读取最新状态并刷写。 + +**修复后时序**: + +``` +T1: API-A 修改 DB +T2: INSERT IGNORE → inserted=0 +T3: onFlushSuccess DELETE → 行被删除 +T4: UPDATE → updated=0 +T5: 重新 INSERT IGNORE → 成功,dirtyVersion=1 +T6: triggerFlushForVm → 认领新行 → flush 读 DB(包含 API-A 的修改)→ 写入 (Y) +``` + +/** + * 便捷重载:默认 storageStructureChange=false(CONFIG 级别)。 + */ +public boolean markDirty(String vmInstanceUuid) { + return markDirty(vmInstanceUuid, false); +} +``` + +### 为什么 markDirty 需要检查 `vm.metadata.enabled`? + +需要检查。虽然 `VmMetadataUpdateInterceptor` 层已检查功能开关,但 markDirty 还有其他调用方(级联删除、HA 回调、巡检恢复等),这些调用方未必都做了检查。在 markDirty 内统一检查是防御性编程的最低成本方案。 + +### 为什么不重置 retryCount? + +如果 PS 持续不可用,连续 API 触发的 markDirty 不应重置重试计数器,否则永远不会触达上限告警。retryCount 仅在**刷写成功**时重置为 0。 + +### 为什么不修改 managementNodeUuid? + +若 Poller 已认领此行正在刷写,markDirty 不应抢走它。`dirtyVersion` 递增后,刷写完成时会通过版本号比较发现“有新变更”,自动释放让下轮重处理(见 §4.5)。 + +### markDirty 后立即唤醒 + +markDirty 后立即调用 `triggerFlushForVm(vmUuid)` 尝试认领并提交刷写,消除最长 5s 的 Poller 等待延迟。Poller 降级为**安全网**,负责处理:退避中的行、MN 宕机后释放的行、triggerFlush 未能认领的行。 + +```java +/** + * 立即尝试认领并刷写指定 VM 的 dirty 行。 + * 若行已被认领或处于退避期,跳过(Poller 安全网会处理)。 + */ +private void triggerFlushForVm(String vmUuid) { + String myId = Platform.getManagementServerId(); + // Q20 修复:findStaleClaimOwner 可能返回 null(无 stale claim)。 + // SQL 的 OR 分支使用 :staleId 参数,当 staleId=null 时 + // MySQL 会将 `managementNodeUuid = NULL` 解析为 FALSE(SQL 三值逻辑), + // 不会误匹配任何行。但为避免依赖此隐式行为,显式处理: + // staleId=null 时仅使用 IS NULL 分支,不包含 stale 接管条件。 + String staleId = findStaleClaimOwner(vmUuid, Duration.ofMinutes(10)); + + String sql; + if (staleId != null) { + sql = "UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE vmInstanceUuid = :vmUuid " + + "AND (managementNodeUuid IS NULL " + + " OR (managementNodeUuid = :staleId AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE)) " + // 10 → vm.metadata.triggerFlush.staleMinutes + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; + } else { + sql = "UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE vmInstanceUuid = :vmUuid " + + "AND managementNodeUuid IS NULL " + + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; + } + + int claimed = SQL.New(sql) + .param("myId", myId) + .param("staleId", staleId) // null-safe: only used when staleId != null + .param("vmUuid", vmUuid) + .execute(); + + if (claimed == 0) { + logger.debug("triggerFlushForVm skip claim, vmUuid={}, reason=already-claimed-or-backoff", vmUuid); // Q2-3 + return; + } + + VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + // DP-07 说明:dirty == null 是合法场景。CAS UPDATE 成功(claimed > 0)后、findByUuid 前, + // 若同 MN 上一个 running flush 的 onFlushSuccess() 恰好执行了条件 DELETE + // (dirtyVersion 未被新 markDirty 递增),则该行已被删除。 + // 此时直接 return 即可,无需告警——数据已经是最新的。 + if (dirty == null) return; + + submitFlushTask(dirty); // 提交到 ChainTask(同 Poller 路径) +} +``` + +**退避中的行不会被立即唤醒**:若 dirty 行处于指数退避(`nextRetryTime > NOW()`),triggerFlush 的 WHERE 条件将其排除。这是有意设计——退避意味着 PS 可能不可用,markDirty 带来的新变更会在退避到期后由 Poller 一并处理。 + +**DP-06 分析:长时间 flush 与 stale claim 接管的交互**(补充说明) + +`triggerFlushForVm()` 的 stale claim 接管阈值为 **10 分钟**,而 `claimDirtyRows()` 的僵尸清理阈值为 **15 分钟**。两者的不对称设计有以下意图: + +| 路径 | 阈值 | 理由 | +|------|------|------| +| `triggerFlushForVm` stale 接管 | 10 min | API 热路径,优先保证响应性 | +| `cleanupZombieClaims` 僵尸清理 | 15 min | 批量路径,保守策略避免误抢 | + +**潜在问题**:若某次 `doFlush` 因 PS 慢响应耗时 8-12 分钟(未超 5 分钟消息超时但含排队等待), +`triggerFlushForVm()` 可能在第 10 分钟接管该行的 claim,而原 flush 任务仍在 ChainTask 中运行, +导致同一 VM 短暂出现两个并发 flush 意图。由于 per-VM ChainTask `syncLevel=1`, +实际执行仍是串行的,不会产生数据不一致。但建议后续考虑引入 `flushStartTime` 字段, +让 stale 判断基于「flush 实际开始时间」而非「claim 时间」,避免误判。 + +## 3.2 调用位置 + +| 调用方 | 场景 | 说明 | +|--------|------|------| +| `VmMetadataUpdateInterceptor.beforePublishEvent()` | `@MetadataImpact` API 成功后 | 主流程 | +| `MetadataCascadeExtension.asyncCascade()` | 级联删除 Volume/Snapshot | 非 API 内部操作 | +| HA handler 完成回调 | HA 重启 VM | 非 API 内部操作 | +| 定时快照清理 handler | 快照删除 | 非 API 内部操作 | +| 内部卷迁移 handler | installPath 变更 | 非 API 内部操作 | +| 升级全量刷新 | 版本变更后批量触发 | 见 §9 | + +**两道防线**: + +1. **开发规范**:修改 VM 存储拓扑字段的内部消息处理器,成功后必须调用 `markDirty()` +2. **路径指纹巡检兜底**:每次刷写成功后记录 VM 的全量路径快照,独立 PeriodicTask 周期性比对 DB 当前路径 vs 快照,不一致则 `markDirty()`(见 §8.2) + +对注册场景,即使元数据暂时落后于 DB,Part 3 §3.4 的 installPath 存在性检查提供额外兗底。 + +与 GC 方案 submit 的详细对比见 [对比文档 §2](2/vm-metadata-new-02h-compare.md#2-标脏入口对比markdirty-vs-gc-submit)。 + +--- + +# 4. MetadataDirtyPoller — 轮询刷写 + +## 4.1 基本结构 + +```java +public class MetadataDirtyPoller implements PeriodicTask { + @Override + public TimeUnit getTimeUnit() { return TimeUnit.SECONDS; } + + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.value(Long.class); + // 默认 5 秒,可通过 GlobalConfig 动态调整 + } + + @Override + public String getName() { return "vm-metadata-dirty-poller"; } + + @Override + public void run() { + claimAndFlush(); + } +} +``` + +启动:在 `managementNodeReady()` 中 `thdf.submitPeriodicTask(new MetadataDirtyPoller())`。 + +GlobalConfig 变更时自动重启 Poller(与 SecurityGroup FailureHostWorker 一致): + +```java +VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.installUpdateExtension((oldValue, newValue) -> { + restartPoller(); +}); +``` + +**Poller 角色定位**:markDirty 后立即调用 `triggerFlushForVm()` 已覆盖常规场景(见 §3.1)。Poller 降级为**安全网**,负责处理: +- 退避中的行(`nextRetryTime` 到期后才能认领) +- MN 宕机后 FK SET_NULL 释放的孤儿行 +- triggerFlushForVm 认领失败的行(已被其他 MN Poller 认领) + +## 4.2 认领(CAS 方式) + +采用 CAS(单条 UPDATE WHERE NULL LIMIT N),比悲观锁更简洁,避免死锁风险。 + +```java +private List claimDirtyRows() { + // DP-05 修复:僵尸 claim 清理从 claimDirtyRows() 提取为独立低频任务。 + // 原实现在每次 Poller 周期(5s)执行带 write-intent 的 UPDATE 扫描, + // 增加了不必要的数据库压力。改为 cleanupZombieClaims() 独立定时执行(见下方)。 + + // Step 1: CAS 原子认领 — 单条 UPDATE 天然原子 + String sql = "UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + + "WHERE managementNodeUuid IS NULL " + + "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP) " + + "ORDER BY lastOpDate ASC, vmInstanceUuid ASC " + // Q17 修复:按最后标脏时间排序(最早变更优先),vmInstanceUuid 作为稳定 tiebreaker + // 原 ORDER BY dirtyVersion ASC 有误:dirtyVersion 是 per-row 值(每行从 1 开始), + // 不同 VM 的 dirtyVersion 无序且可能相等,无法反映全局标脏顺序。 + // lastOpDate 由 MySQL ON UPDATE CURRENT_TIMESTAMP 自动维护,反映最近一次 markDirty 时间, + // 适合作为公平调度指标。秒级精度足够(Poller 周期 5s >> 1s 精度)。 + "LIMIT :batchSize"; + + int claimed = SQL.New(sql) + .param("myId", Platform.getManagementServerId()) + .param("batchSize", VmGlobalConfig.VM_METADATA_DIRTY_BATCH_SIZE.value(Integer.class)) + .execute(); + + if (claimed == 0) return Collections.emptyList(); + + // Step 2: 查询刚认领到的行(DP-01 修复:增加 lastClaimTime 过滤, + // 仅返回本轮 CAS 认领的行,避免与 triggerFlushForVm 并发认领的行混入) + // Q18 说明:thisCycleCutoff = now - 5s 是 Poller 周期的上界。若某轮 Poller 执行(含 CAS UPDATE) + // 耗时超过 5s(极端负载),cutoff 可能过滤掉本轮认领的行。但这仅导致那些行不被本轮处理, + // 下轮 Poller 仍会发现它们(已 claimed by this MN + lastClaimTime 匹配)。 + // 更精确的做法是在 Step 1 CAS 前记录 beforeClaim = CURRENT_TIMESTAMP, + // Step 2 使用 gte(lastClaimTime, beforeClaim)。但引入 Java↔DB 时间偏差风险。 + // 当前方案的 5s 余量足够覆盖 99.99% 场景,接受此权衡。 + Timestamp thisCycleCutoff = Timestamp.from(Instant.now().minus(Duration.ofSeconds(5))); + return Q.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.managementNodeUuid, Platform.getManagementServerId()) + .gte(VmMetadataDirtyVO_.lastClaimTime, thisCycleCutoff) + .list(); +} +``` + +**DP-05 改进:僵尸 claim 清理独立为低频任务** + +```java +/** + * 独立的僵尸 claim 清理任务(防御性措施)。 + * 从 claimDirtyRows() 提取,以避免每 5s Poller 周期执行不必要的 write-intent 扫描。 + * 建议间隔:60s(1 分钟),远低于 15 分钟僵尸阈值,足以及时发现异常。 + * + * 僵尸清理的必要性分析: + * MN 正常崩溃 → FK SET NULL 立即释放 claim,Poller 下轮即可重认领。 + * 本任务覆盖的是 FK SET NULL 无法触发的场景: + * (a) MN 进程 hang 住(JVM 死锁 / 长 GC),心跳未失效但 flush 永久阻塞; + * (b) 网络分区导致目标 Agent 无响应,ChainTask 在 timeout 前持续持有 claim; + * (c) 极端:MN 已离线但 ManagementNodeVO 记录因 heartbeat 延迟尚未被清理。 + * 15 分钟阈值 > flush 最大超时(5×60s=5min),安全余量充足。 + */ +@PeriodicTask(interval = 60, unit = TimeUnit.SECONDS) +private void cleanupZombieClaims() { + SQL.New("UPDATE VmMetadataDirtyVO " + + "SET managementNodeUuid = NULL, lastClaimTime = NULL " + + "WHERE managementNodeUuid IS NOT NULL " + + "AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL 15 MINUTE") + .execute(); +} +``` + +**说明**:`triggerFlushForVm()` 单 VM 抢占路径允许"stale claim 接管"(10 分钟),Poller 批量路径采用"先清理僵尸再 CAS"的保守策略(15 分钟),避免误抢活跃任务。 + +**CAS vs 悲观锁**: + +| | CAS (UPDATE WHERE NULL) | 悲观锁 (SELECT FOR UPDATE) | +|---|---|---| +| 原子性 | 单条 UPDATE 天然原子 | 需事务包裹 SELECT + UPDATE | +| 死锁风险 | 无 | 双 MN 可能死锁 | +| 性能 | 无锁等待 | 有锁等待 | +| 实现复杂度 | 低 | 中 | + +**MySQL 行锁分析**:CAS 的 `UPDATE ... WHERE managementNodeUuid IS NULL AND ... LIMIT N` 在 InnoDB 中会对满足 WHERE 条件的行加 **X 锁**(排他锁)。双 MN 并发执行时,先执行的 UPDATE 获得行锁并将 `managementNodeUuid` 置为非 NULL,后执行的 UPDATE 的 WHERE 条件不再匹配该行,`affected_rows=0`。`LIMIT N` 保证每次最多锁定 N 行,并发窗口极短(微秒级),不会引发死锁。 + +## 4.3 刷写(Flush) + +认领成功后,对每个 dirty row 提交到 ChainTask 执行刷写: + +```java +private void claimAndFlush() { + List claimed = claimDirtyRows(); + for (VmMetadataDirtyVO dirty : claimed) { + submitFlushTask(dirty); + } +} + +private void submitFlushTask(VmMetadataDirtyVO dirty) { + // 讨论 Δ-1:原方案为嵌套 ChainTask(外层全局限流 + 内层 per-VM 串行)。 + // 重构为单层 per-VM ChainTask + AtomicInteger 全局限流,原因: + // 1. 嵌套 ChainTask 的 outerChain.next() 必须在 innerChain 完成后调用, + // 但 exceedMaxPendingCallback 中 outerChain.next() 直接调用导致 + // outer slot 提前释放,全局限流语义被破坏。 + // 2. 嵌套结构难以推断 Chain 生命周期,增加维护和调试成本。 + // 3. AtomicInteger 全局计数器语义简单明确:flush 开始 increment、 + // 完成(成功/失败/exceed)decrement,超限时 skip。 + // + // 新结构: + // - 全局 AtomicInteger globalFlushInFlight(初始 0) + // - submitFlushTask 先检查 globalFlushInFlight < maxConcurrent, + // 超限时释放 claim 并 return + // - 通过则 increment,提交到 per-VM ChainTask(syncLevel=1, maxPending=1) + // - doFlush 完成回调中 decrement + + int maxConcurrent = VmGlobalConfig.VM_METADATA_GLOBAL_MAX_CONCURRENT.value(Integer.class); + if (globalFlushInFlight.get() >= maxConcurrent) { + // 全局并发已满,释放 claim,Poller 下轮重试 + releaseClaim(dirty.getVmInstanceUuid()); + return; + } + globalFlushInFlight.incrementAndGet(); + + // 单层 per-VM 串行 + 去重 + thdf.chainSubmit(new ChainTask(null) { + @Override + public String getSyncSignature() { + return "update-vm-" + dirty.getVmInstanceUuid() + "-metadata"; + } + @Override + public int getSyncLevel() { return 1; } + @Override + public int getMaxPendingTasks() { return 1; } + @Override + public String getDeduplicateString() { return getSyncSignature(); } + + @Override + public void exceedMaxPendingCallback() { + // 已有 running + pending,本次多余 + // Δ-1 改进:在单层结构中,exceed 时直接 decrement 并释放 claim + globalFlushInFlight.decrementAndGet(); + releaseClaim(dirty.getVmInstanceUuid()); + } + + @Override + public void run(SyncTaskChain chain) { + doFlush(dirty, () -> { + globalFlushInFlight.decrementAndGet(); + chain.next(); + }); + } + }); +} +``` + +## 4.4 doFlush 核心逻辑 + +```java +private void doFlush(VmMetadataDirtyVO dirty, Runnable chainNext) { + String vmUuid = dirty.getVmInstanceUuid(); + + // P2 修复:重新从 DB 读取 dirty 行,获取最新的 storageStructureChange 和 dirtyVersion。 + // 原因:submitFlushTask 传入的 dirty 对象是 CAS 认领时的缓存快照,排队等待期间 + // 可能有新的 markDirty(storageStructureChange=true) 通过 OR 升级了该字段。 + // 若使用缓存值,会导致本轮 flush 的 storageStructureChange=false, + // 而 DB 中实际已为 true(如存储迁移触发的 markDirty),写入时用错 tmp 后缀。 + VmMetadataDirtyVO latestDirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + if (latestDirty == null) { + // VM 已删除(FK CASCADE)或 onFlushSuccess 已删除该行 + chainNext.run(); + return; + } + + // 0. 记录刷写开始时的 dirtyVersion 快照(使用最新值) + long snapshotVersion = latestDirty.getDirtyVersion(); + + // 1. 前置检查:VM 是否存在 + if (!dbf.isExist(vmUuid, VmInstanceVO.class)) { + // VM 已删除,FK CASCADE 应已删除 dirty 行,兜底删除 + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); + chainNext.run(); + return; + } + + // 1b. Q34 修复:过滤 Destroyed 状态的 VM + // VM 正在销毁过程中(state=Destroyed),EO 尚未物理删除,FK CASCADE 未触发。 + // 此时刷写元数据无意义——销毁完成后 EO 删除时 dirty 行会被级联清理。 + // 主动删除 dirty 行释放 Poller 资源,避免对即将销毁的 VM 执行无效的 Agent 调用。 + VmInstanceState vmState = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmUuid) + .select(VmInstanceVO_.state) + .findValue(); + if (vmState == VmInstanceState.Destroyed) { + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); + chainNext.run(); + return; + } + + // 2. 发送 UpdateVmInstanceMetadataMsg → VmInstanceBase 负责构建 payload 并写入主存储 + // payload 构建(buildVmInstanceMetadata)和大小保护均在 VmInstanceBase 内部完成 + UpdateVmInstanceMetadataMsg msg = new UpdateVmInstanceMetadataMsg(); + msg.setUuid(vmUuid); + msg.setStorageStructureChange(latestDirty.isStorageStructureChange()); + msg.setTimeout(TimeUnit.MINUTES.toMillis(5)); + bus.makeLocalServiceId(msg, VmInstanceConstant.SERVICE_ID); + + bus.send(msg, new CloudBusCallBack(null) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + onFlushSuccess(vmUuid, snapshotVersion); + } else { + onFlushFailure(vmUuid, reply.getError()); + } + chainNext.run(); + } + }); +} +``` + +## 4.5 刷写成功处理 + +```java +// DP-04 修复 + 讨论 Δ-2:原方案使用 @Transactional 包装 DELETE + fallback UPDATE。 +// 改为 SQLBatch 替代 @Transactional,原因: +// 1. @Transactional 由 Spring AOP 代理实现,要求方法为 public 且通过代理对象调用。 +// onFlushSuccess 作为内部回调方法,直接调用(this.onFlushSuccess)不经过代理, +// @Transactional 不生效("self-invocation 陷阱")。 +// 2. SQLBatch 是 ZStack 原生事务工具,无代理依赖,显式包装事务边界, +// 在 callback/lambda 场景中更可靠。 +// 3. 逻辑不变:DELETE + fallback UPDATE 仍在同一事务内原子执行。 +private void onFlushSuccess(String vmUuid, long snapshotVersion) { + new SQLBatch() { + @Override + protected void scripts() { + // 条件删除:仅当 dirtyVersion == snapshotVersion 时删除 + // 即"刷写期间没有新的 markDirty 到来" + int deleted = SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .eq(VmMetadataDirtyVO_.dirtyVersion, snapshotVersion) + .delete(); + + if (deleted == 0) { + // dirtyVersion > snapshotVersion → 刷写期间有新变更 + // 释放认领,让 triggerFlush / Poller 重新处理 + // 同时重置 retryCount(本次成功说明通路正常) + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) + .set(VmMetadataDirtyVO_.retryCount, 0) + .set(VmMetadataDirtyVO_.nextRetryTime, null) + .update(); + } + // deleted > 0 → 行已删除,彻底完成 + } + }.execute(); + + // 讨论 Δ-9:savePathFingerprint 复用 buildVmInstanceMetadata 的 installPath 列表。 + // 原方案在 savePathFingerprint 中独立查询 VolumeVO + VolumeSnapshotVO。 + // 改为 buildVmInstanceMetadata 返回复合对象(含 payload + installPath list), + // onFlushSuccess 直接传入 installPath list 给 savePathFingerprint, + // 避免重复查询,减少一次 DB roundtrip。 + // 具体实现:doFlush 中 buildVmInstanceMetadata 返回 BuildResult{payload, pathSnapshot}, + // onFlushSuccess(vmUuid, snapshotVersion, pathSnapshot) 传入预计算的 pathSnapshot。 + savePathFingerprint(vmUuid); +} +``` + +**这是整个方案最关键的设计点**。`dirtyVersion` 比较确保不会丢失刷写期间产生的新变更: + +``` +T0: markDirty(vm-1) → INSERT, dirtyVersion=1 +T1: 认领 → snapshotVersion=1 +T2: 刷写进行中... buildVmInstanceMetadata() 读到 v1 +T3: API 成功 → markDirty(vm-1) → dirtyVersion=2 ← 新变更! +T4: 刷写完成,写入 v1 +T5: onFlushSuccess → DELETE WHERE dirtyVersion = 1 + → 当前 dirtyVersion=2 ≠ 1 → deleted=0 + → 释放认领 → triggerFlush 立即重处理 → 读到 v2 → 写入 v2 (Y) +``` + +如果不做 `dirtyVersion` 比较直接删除,T3 的变更就丢了——这正是 GC `deduplicateSubmit` 遇到的同类问题,新方案用版本号比较优雅解决。相比 `lastOpDate` 时间戳比较,`dirtyVersion` 整数比较语义更明确、无时间精度问题。 + +## 4.6 刷写失败处理 + +```java +private void onFlushFailure(String vmUuid, ErrorCode error) { + // Q21 — 原子性分析:先 findByUuid 再 UPDATE 存在微窗口(读到的 retryCount 可能被 + // 并发 markDirty 改变)。但 markDirty 不修改 retryCount(仅递增 dirtyVersion), + // 且同一 VM 同一时刻只有一个 flush 任务(Layer 1 CAS + Layer 3 per-VM syncLevel=1), + // 因此 onFlushFailure 的 findByUuid→UPDATE 在同 VM 上无并发竞争。安全。 + VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); + if (dirty == null) return; // VM 已销毁,FK CASCADE 已清理 + + int newRetryCount = dirty.getRetryCount() + 1; + int maxRetry = VmGlobalConfig.VM_METADATA_MAX_RETRY.value(Integer.class); // 默认 5 + int baseDelay = VmGlobalConfig.VM_METADATA_RETRY_BASE_DELAY_SECONDS.value(Integer.class); // Q2-6 + int maxExponent = VmGlobalConfig.VM_METADATA_RETRY_MAX_EXPONENT.value(Integer.class); // Q2-6 + + if (newRetryCount >= maxRetry) { + // 达到上限 → 告警 + 标记 stale(H2 修复:不再直接删除) + logger.error("metadata update for vm {} failed after {} retries, marking as stale. " + + "MetadataStaleRecoveryTask will retry independently.", + vmUuid, newRetryCount); + + // 在 PathFingerprintVO 上标记 lastFlushFailed=true(M1 修复) + SQL.New("UPDATE VmMetadataPathFingerprintVO " + + "SET lastFlushFailed = 1 WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", vmUuid) + .execute(); + + // 删除 dirty 行(释放 Poller 资源),stale 恢复由独立任务接管 + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); + return; + } + + // 未达上限 → 释放认领 + 指数退避(Q2-6: 参数改为 GlobalConfig) + long delaySec = baseDelay * (1L << Math.min(newRetryCount, maxExponent)); + Timestamp nextRetry = Timestamp.from(Instant.now().plusSeconds(delaySec)); + + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) // 释放认领 + .set(VmMetadataDirtyVO_.retryCount, newRetryCount) + .set(VmMetadataDirtyVO_.nextRetryTime, nextRetry) + .update(); + + logger.warn("metadata update for vm {} failed (retry {}/{}), next retry at {}", + vmUuid, newRetryCount, maxRetry, nextRetry); +} +``` + +**指数退避表**: + +| 尝试次数 | retryCount 变化 | 下次退避延迟 | 累计耗时 | +|----------|-----------------|-------------|----------| +| 1 | 0 → 1 | 20s | ~25s | +| 2 | 1 → 2 | 40s | ~65s | +| 3 | 2 → 3 | 80s | ~145s | +| 4 | 3 → 4 | 160s | ~305s | +| 5 | 4 → 5 | — | 放弃 | + +延迟公式:`vm.metadata.retry.baseDelaySeconds × 2^min(retryCount, vm.metadata.retry.maxExponent)`;默认值分别为 `10`、`10`。默认 5 次重试,总耗时约 5 分钟后放弃。 + +## 4.7 辅助方法 + +```java +private void releaseClaim(String vmUuid) { + SQL.New(VmMetadataDirtyVO.class) + .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) + .set(VmMetadataDirtyVO_.managementNodeUuid, null) + .update(); +} +``` + +## 4.8 Stale 恢复任务(H2 修复) + +当 dirty 行因重试耗尽被删除后,低频 VM(长期无 `@MetadataImpact` API)将失去自愈机会。为此引入独立的 `MetadataStaleRecoveryTask`: + +```java +public class MetadataStaleRecoveryTask implements PeriodicTask { + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_STALE_RECOVERY_INTERVAL.value(Long.class); + // 默认 1800 秒(30 分钟) + } + + @Override + public void run() { + // 查找所有 lastFlushFailed=true 的指纹记录 + List staleVms = SQL.New( + "SELECT fp FROM VmMetadataPathFingerprintVO fp WHERE fp.lastFlushFailed = 1", + VmMetadataPathFingerprintVO.class) + .limit(VmGlobalConfig.VM_METADATA_STALE_RECOVERY_BATCH.value(Integer.class)) // 默认 100 + .list(); + + for (VmMetadataPathFingerprintVO fp : staleVms) { + // 重新 markDirty,给予全新的重试机会(retryCount=0) + // DP-03 修复:先验证 markDirty 成功,再清除 stale 标记; + // 若 markDirty 失败(如 DB 连接异常),保留 lastFlushFailed=true, + // 下一轮 StaleRecoveryTask 会重试。 + boolean markSuccess = markDirty(fp.getVmInstanceUuid()); + if (markSuccess) { + // markDirty 成功 → 安全清除 stale 标记(由下轮 Poller 处理) + SQL.New("UPDATE VmMetadataPathFingerprintVO " + + "SET lastFlushFailed = 0 WHERE vmInstanceUuid = :vmUuid") + .param("vmUuid", fp.getVmInstanceUuid()) + .execute(); + } else { + // markDirty 失败 → 保留 lastFlushFailed=true,记录日志 + logger.warn("MetadataStaleRecoveryTask: markDirty failed for vm={}, " + + "keeping lastFlushFailed=true for next retry cycle", + fp.getVmInstanceUuid()); + } + } + + if (!staleVms.isEmpty()) { + logger.info("MetadataStaleRecoveryTask re-queued {} stale VMs for retry", staleVms.size()); + } + } +} +``` + +**关键设计点**: +- 独立于 Poller 的 PeriodicTask,不受 Poller 退避机制约束 +- 每 30 分钟扫描一次,每次最多处理 100 个 stale VM +- 重新 `markDirty()` 给予全新重试机会(retryCount=0),不继承历史退避 +- **DP-03 修复**:`markDirty()` 返回 boolean,仅在成功时才清除 `lastFlushFailed`;失败时保留标记,下轮再试 +- 若 PS 仍不可用,该 VM 会再次走完 Poller 的 5 次重试 → 再次标记 stale → 30 分钟后再次恢复,形成"慢速重试"闭环 +- 当 PS 恢复可用时,下一轮 stale recovery 触发的 markDirty 自然成功 + +--- + +# 5. 消息调用链 + +## 5.1 新调用链 + +``` +API (e.g. StartVmInstanceMsg) 成功 + ↓ +VmMetadataUpdateInterceptor.beforePublishEvent() + ↓ +markDirty(vmUuid) ← INSERT/UPDATE + dirtyVersion++,本地操作,无跨 MN + ↓ +triggerFlushForVm(vmUuid) ← 立即唤醒:CAS 认领单行 + 提交 ChainTask + ↓(认领失败时由 Poller 安全网兆底,≤5s) + ↓ AtomicInteger globalFlushInFlight 检查(Δ-1:替代原嵌套 ChainTask 外层) + ↓ per-VM ChainTask "update-vm-{vmUuid}-metadata" (syncLevel=1, maxPending=1) + ↓ +doFlush() + → bus.send(UpdateVmInstanceMetadataMsg) → makeLocalServiceId + ↓ +VmInstanceBase.handle(UpdateVmInstanceMetadataMsg) + → buildVmInstanceMetadata(vmUuid) — DB 全量读取(@Transactional(readOnly=true)) + → payload 大小保护(>8MB 告警, >30MB 拒绝) + ↓ +bus.send(UpdateVmInstanceMetadataOnPrimaryStorageMsg) → makeLocalServiceId + ↓ +NFS/LocalStorage/SharedBlock.handle() + ↓ ChainTask "update-metadata-on-ps-{psUuid}" + ↓ 选取 Host → UpdateVmInstanceMetadataOnHypervisorMsg + ↓ makeTargetServiceIdByResourceUuid(hostUuid) ← 保留 hash 环路由 + ↓ +HostBase.handle() → HTTP call to KVM agent + ↓ +成功 → onFlushSuccess() → 条件 DELETE +失败 → onFlushFailure() → 指数退避释放 +``` + +**OP type 由管理层面指定**:`@MetadataImpact(CONFIG)` → OP type=1(仅配置变更),`@MetadataImpact(STORAGE)` → OP type=2(存储拓扑变更,sblk 场景设置 pending_op=2)。OP type 通过 `storageStructureChange` 字段贯穿整条消息链(`VmMetadataDirtyVO` → `UpdateVmInstanceMetadataMsg` → `UpdateVmInstanceMetadataOnPrimaryStorageMsg` → `UpdateVmInstanceMetadataOnHypervisorMsg`)。dirty 行使用 OR 升级策略:多次 markDirty 中只要有一次是 STORAGE,本轮刷写即使用 OP type=2。 + +**消息超时**:`UpdateVmInstanceMetadataMsg` 设置为 `5min`(防止内层任务 hang 导致 claim 长期占用);`UpdateVmInstanceMetadataOnHypervisorMsg` 保持 `2min`。超时后统一进入 `onFlushFailure()` 释放认领并退避重试。 + +与 GC 方案消息链的详细对比见 [对比文档 §3](2/vm-metadata-new-02h-compare.md#3-消息调用链对比)。 + +## 5.2 消息路由策略 + +| 消息 | 路由方式 | 说明 | +|------|----------|------| +| `UpdateVmInstanceMetadataMsg` | `makeLocalServiceId` | Poller 本地发起 | +| `UpdateVmInstanceMetadataOnPrimaryStorageMsg` | `makeLocalServiceId` | 无本地状态依赖 | +| `UpdateVmInstanceMetadataOnHypervisorMsg` | `makeTargetServiceIdByResourceUuid(hostUuid)` | 需路由到 host-owner MN | + +--- + +# 6. 并发控制(四层) + +## 6.1 四层串行化保证 + +``` +Layer 1 — DB CAS 认领 + UPDATE WHERE managementNodeUuid IS NULL → 同一行只被一个 MN 处理 + ⇒ 同一 VM 的刷写不会在两个 MN 上同时执行 + +Layer 2 — 全局限流(AtomicInteger) + globalFlushInFlight AtomicInteger (默认上限 10,可通过 GlobalConfig 调整) + ⇒ 同一 MN 最多 N 个 VM 同时更新 + 讨论 Δ-1 变更:原方案为嵌套 ChainTask 外层全局队列, + 改为 AtomicInteger 计数器。语义等价但消除了嵌套 Chain 的复杂性。 + submitFlushTask 入口检查 get() >= maxConcurrent 时直接 releaseClaim 跳过。 + +Layer 3 — per-VM 串行队列 "update-vm-{vmUuid}-metadata" + syncLevel=1, maxPendingTasks=1 + ⇒ 同一 VM 最多 1 个正在执行 + 1 个排队 + ⇒ 超出时 exceedMaxPendingCallback() → decrementAndGet + releaseClaim + +Layer 4 — 主存储级队列 "update-metadata-on-ps-{psUuid}" + syncLevel = vm.metadata.ps.maxConcurrent (GlobalConfig, 默认 5) + ⇒ 同一 MN 上,同一存储最多 N 个并发写入 + ⇒ 双 MN 环境下实际全局并发 = 2 × syncLevel +``` + +与 GC 方案并发控制的详细对比见 [对比文档 §4](2/vm-metadata-new-02h-compare.md#4-并发控制对比)。 + +## 6.2 全局限流 + +**讨论 Δ-1 重构后结构**: + +原嵌套 ChainTask 结构已简化为单层结构: + +``` +AtomicInteger globalFlushInFlight (上限 = vm.metadata.global.maxConcurrent, 默认 10) + └── per-VM ChainTask: syncSignature = "update-vm-{vmUuid}-metadata" + syncLevel = 1, maxPendingTasks = 1, deduplicateString = syncSignature +``` + +- `globalFlushInFlight` 控制全局并发数,每个 MN 最多 N 个 VM 同时更新 +- per-VM ChainTask 保证 per-VM 串行 + 去重 +- `exceedMaxPendingCallback` 中直接 `decrementAndGet()` + `releaseClaim()`,不再持有 claim + +**per-MN 语义**:`globalFlushInFlight` 是 JVM 本地计数器。双 MN 环境下实际全局并发最大为 `2 × maxConcurrent`。DB CAS 认领已保证同一 VM 不会在两个 MN 上同时执行。 + +## 6.3 Layer 3 实现位置 + +各主存储 `handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg)` 内部用 `thdf.chainSubmit()` 包装: + +- `getSyncSignature()` → `"update-metadata-on-ps-" + self.getUuid()` +- `getSyncLevel()` → 读取 `VmGlobalConfig.VM_METADATA_PS_MAX_CONCURRENT` +- `run()` → 调用实际写入逻辑后 `chain.next()` + +**外层全局计数器与 Layer 4 的交互**:AtomicInteger `globalFlushInFlight` 上限为 10(默认),限制单个 MN 上同时最多 10 个 VM 的元数据更新在执行。这 10 个并发任务分布在不同主存储上时,Layer 4 per-PS 队列 `syncLevel=5` 进一步约束同一存储的并发数。AtomicInteger 控制"总水位",Layer 4 控制"每个 PS 的分水位",二者共同生效。 + +**文档化要求**:`syncLevel` 和 AtomicInteger 全部为 JVM 本地语义。跨 MN 并发由 DB CAS 认领控制,不通过 ChainTask 全局共享队列。 + +## 6.4 调优指南 + +### 默认值推导 + +- `batchSize=50`:按平均每 VM flush 200ms 估算,50 台约 10s/轮;实际耗时受 `global.maxConcurrent=10` 并行限制。 +- `global.maxConcurrent=10`:管理节点线程池默认 500 线程,10 个并发约占 2%,对其他业务影响可控。 +- `ps.maxConcurrent=5`:限制单主存储写入并发,避免元数据 flush 风暴挤占业务 IO。 + +### 调优参考表 + +| 环境规模 | VM 数量 | batchSize | global.maxConcurrent | ps.maxConcurrent | pollInterval | +|----------|---------|-----------|----------------------|------------------|--------------| +| 小型 | <500 | 50 | 10 | 5 | 5s | +| 中型 | 500-5000 | 100 | 20 | 10 | 5s | +| 大型 | >5000 | 200 | 30 | 15 | 10s | + +### 调优顺序建议 + +1. 先调 `global.maxConcurrent`(观察 MN CPU/线程池饱和度); +2. 再调 `ps.maxConcurrent`(观察单 PS 延迟与业务 IO 干扰); +3. 最后调 `batchSize` 与 `pollInterval`(平衡吞吐与扫描开销)。 + +--- + +**文档拆分**:§7-§13(高可用、恢复策略、升级刷新、Payload 保护、开发约束、GlobalConfig)已迁移至 [Part 2b — 高可用与运维](vm-metadata-02b-高可用与运维.md)。 + +# 7. 约束与不変量 + +| 约束 ID | 约束描述 | 违反后果 | +|---------|----------|----------| +| C-DM-01 | `markDirty` 在集群模式下必须使用 `INSERT IGNORE + UPDATE` 两步,禁止回退为 `INSERT ON DUPLICATE KEY`。当 `inserted==0 && updated==0` 时必须重新 `INSERT IGNORE` 防止竞态丢失。**例外**:升级全量刷新场景(Part 2b §9)中批量 `markDirty` 可使用等效的批量 INSERT IGNORE + 批量 UPDATE 优化,但必须保持「先 INSERT IGNORE 再 UPDATE」的两步语义 | Galera 高并发下死锁概率上升,标脏链路抖动;竞态下 DB 变更丢失不被刷写 | +| C-CL-02 | 任何 claim 成功路径必须写入 `lastClaimTime`,并执行僵尸 claim 清理(15 分钟)。注:僵尸清理已独立为低频任务 `cleanupZombieClaims()`(DP-05) | hang 任务可能导致 dirty 行永久锁定 | +| C-TM-03 | `doFlush` 消息超时不得低于 5 分钟,且超时必须进入 `onFlushFailure` 释放 claim | inner task 卡死时无法自愈 | +| C-RB-04 | 指数退避参数必须来自 GlobalConfig(baseDelay/maxExponent),禁止硬编码常量 | 运维无法按环境调优重试节奏 | +| C-SR-05 | 重试耗尽时必须在 `VmMetadataPathFingerprintVO` 标记 `lastFlushFailed=true`,不得仅删除 dirty 行后静默放弃 | Stale VM 永久失去自愈路径 | +| C-SR-06 | `MetadataStaleRecoveryTask` 的 `markDirty()` 必须使用 retryCount=0(全新起点),不得继承历史退避。同时必须验证 `markDirty()` 返回值,仅在成功时清除 `lastFlushFailed`(DP-03) | 历史退避会导致立即再次耗尽;无条件清除可能永久丢失 stale 标记 | +| C-SC-07 | `storageStructureChange` 标记仅在真正影响存储拓扑的操作中设置(卷创建/删除/迁移/挂载/卸载),不得在纯属性修改(如改名、改描述)时误设。升级全量刷新场景中,`storageStructureChange` 应始终为 `true`(因为无法判断升级前后存储拓扑是否变化) | 误设 true → 触发不必要的 sblk 存储拓扑重建,增加 IO 开销;误设 false → 升级后存储拓扑变更未反映到 sblk | +| C-FL-08 | `doFlush` 必须在前置检查中过滤 `VmInstanceVO.state == Destroyed` 的 VM,主动删除 dirty 行释放 Poller 资源 | 对即将销毁的 VM 执行无效 Agent 调用,浪费资源并可能因 VM 关联存储正在清理而失败 | diff --git "a/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" "b/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" new file mode 100644 index 00000000000..62cb071fb81 --- /dev/null +++ "b/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" @@ -0,0 +1,942 @@ +# VM 元数据 — 高可用与运维 + +## 目录 + +7. [双 MN 高可用](#7-双-mn-高可用) +8. [管理平面恢复策略](#8-管理平面恢复策略) +9. [升级后全量刷新](#9-升级后全量刷新) +9a. [功能开关切换处理](#9a-功能开关切换处理) +10. [Payload 大小保护](#10-payload-大小保护) +11. [潜在代价与 tradeoff](#11-潜在代价与-tradeoff) +12. [开发约束清单](#12-开发约束清单) +13. [GlobalConfig 配置项汇总](#13-globalconfig-配置项汇总) +14. [可观测性指标](#14-可观测性指标) +15. [约束与不変量](#15-约束与不変量) + +**注意**:章节编号保持与原 Part 2 一致(§7-§13),以保证跨文档引用不变。§1-§6(数据模型、markDirty、Poller、消息链、并发控制)见 [Part 2 — Dirty Mark + Poller](vm-metadata-02-脏标记与Poller.md)。 + +--- + +# 7. 双 MN 高可用 + +## 7.1 为什么不需要 hash 环路由 + +`VmMetadataDirtyVO` 是 **共享 DB 表**,两个 MN 的 Poller 都能看到。认领通过 **DB CAS** 保证互斥,不依赖 JVM 本地状态——谁先认领谁处理,无需协调"谁是 owner"。 + +## 7.2 MN 宕机场景(自动恢复) + +``` +T0: MN-A Poller 认领 dirty(vm-1) + DB: {vmUuid:vm-1, managementNodeUuid:MN-A} + +T1: MN-A 宕机 + +T2: MN-B 心跳检测 → 删除 ManagementNodeVO(MN-A) + FK ON DELETE SET NULL → dirty(vm-1).managementNodeUuid = NULL + ← DB 约束自动完成,无需任何代码! + +T3: MN-B nodeLeft(MN-A) → 延迟 5s 后触发一轮 Poller + → 发现 vm-1 未认领 → CAS 认领 → 刷写 (Y) +``` + +**接管延迟**:心跳超时(~30s) + nodeLeft 延迟 5s 触发 ≈ **~35 秒** + +**M2 修复 — 延迟可配**:`nodeLeft` 延迟已通过 `vm.metadata.nodeLeft.delaySec`(§13)配置化(默认 5s)。对于网络抖动频繁的环境,运维可适当增大此值(如 10s)以扩大 in-flight flush 收敛窗口;对于需要快速接管的场景可减小至 3s。调整需与 Fence Check(§7.6)配合评估。 + +增加 `nodeLeft` 回调加速,但引入固定 5s 延迟避免与 dying MN 的 in-flight flush 窗口重叠。 + +```java +@Override +public void nodeLeft(ManagementNodeInventory inv) { + // MN 宕机 → FK SET_NULL 已释放其认领的 dirty 行 + // 延迟 5s 再触发,给 dying MN 的 in-flight flush 收敛窗口 + thdf.submit(() -> { + TimeUnit.SECONDS.sleep(5); + claimAndFlush(); + }); +} + +@Override +public void nodeJoin(ManagementNodeInventory inv) { + // 无需特殊处理,新 MN 的 Poller 正常启动即可 +} + +@Override +public void iAmDead(ManagementNodeInventory inv) { + // 本 MN 即将死亡,不做处理 + // FK SET_NULL 会自动释放本 MN 认领的行 +} + +@Override +public void iJoin(ManagementNodeInventory inv) { + // 由 managementNodeReady 启动 Poller +} +``` + +**接管延迟**:心跳超时(~30s) + nodeLeft 延迟 5s 触发 ≈ **~35 秒**。 + +**最大锁定时间分析**:dirty 行被认领后的最大锁定时间 = MN 心跳超时(默认约 60s) + Poller 间隔(默认 5s)= **~65s**。若 JVM GC pause < 60s,MN 仍存活,dirty 行在 pause 后继续处理;若 GC pause 超过心跳超时 → MN 被判定离线 → FK SET_NULL 释放认领 → 对端 MN 接管。 + +## 7.3 MN 加入场景(无影响) + +``` +T0: MN-A 独自运行,Poller 认领并处理所有 dirty 行 +T1: MN-B 加入 +T2: MN-B Poller 启动 → 与 MN-A Poller 并行运行 + → 两个 Poller 竞争认领 → DB CAS 保证互斥 → 自然负载均衡 +``` + +无需任何特殊处理。两个 Poller 天然分摊工作。 + +## 7.4 双 MN 负载分配 + +两个 MN 的 Poller 并行运行,通过 DB CAS 自然竞争: + +- CAS `UPDATE ... WHERE managementNodeUuid IS NULL LIMIT N` → 每个 MN 各抢到一部分 +- 负载分配取决于 Poller 执行时机,不保证精确 50/50 + +通常不需要精确均匀分配。如需更均匀可在 claim 查询中按 vmUuid 分片(`vmUuid % 2 = mnIndex`),但这引入了对 MN 数量的依赖,不推荐。 + +## 7.5 时序验证 + +### 正常态 + +``` +MN-A: API 成功 → markDirty(vm-1) → INSERT dirty 行 +MN-B: Poller → CAS claim → flush → 成功 → DELETE (Y) +→ 任何一个 MN 都可以处理任何 VM 的 dirty 行 (Y) +``` + +### MN 宕机 + +``` +T0: MN-A claim dirty(vm-1), 正在刷写 +T1: MN-A 宕机 +T~30: MN-B 心跳检测 → 删除 ManagementNodeVO(A) + → FK SET_NULL → dirty(vm-1).managementNodeUuid = NULL +T~35: MN-B nodeLeft(A) → 延迟 5s 后触发 claimAndFlush() + → CAS claim vm-1 → flush → 成功 → DELETE (Y) +``` + +## 7.6 Zombie MN 防护(Fence Check) + +GC pause 场景下,MN-A 可能被判定离线后又恢复执行旧任务。为避免 A/B 并发写同一 VM,在真正写 sblk 前增加认领围栏检查(QX-2): + +```java +// doFlush() 内,在发送 Agent 写请求前 +VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); +if (dirty == null || !Platform.getManagementServerId().equals(dirty.getManagementNodeUuid())) { + logger.warn("Lost claim on vm {}, abort flush write", vmUuid); + return; +} +``` + +说明:fence check 之后到实际 pwrite 之间仍存在微窗口,最终一致性由 sblk 双 Slot + `WriteSequence` 单调递增兜底(读取选择更高 SeqNum)。 + +**M2 修复 — Fence Check 强化说明**: + +Fence Check 的设计目的是**缩小** zombie MN 与接管 MN 并发写入的窗口,而非完全消除。完全消除需要分布式锁(如 etcd lease),成本不可接受。当前方案的安全性层次: + +| 防护层 | 机制 | 窗口 | +|--------|------|------| +| Layer 1 | DB CAS 认领互斥 | 正常场景下无并发 | +| Layer 2 | Fence Check(dirty 行认领验证) | 仅 GC pause 后恢复的极端场景 | +| Layer 3 | sblk 双 Slot + WriteSequence 单调递增 | 即使并发写入,读取侧选择更高 SeqNum,保证最终一致 | +| Layer 4 | `nodeLeft` 延迟(默认 5s,§13 可配) | 降低 Layer 2 场景出现概率 | + +**运维建议**:若监控发现 `Lost claim on vm` 日志频率升高,应检查 GC 配置或增大 `vm.metadata.nodeLeft.delaySec`。 + +**nodeLeft 5s + Fence Check 微窗口的残余风险分析**: +MN-A GC pause 恢复后可能在 Fence Check 通过与 pwrite 之间的微窗口内执行写入,同时 MN-B 的 nodeLeft 延迟 claimAndFlush 也在写入。此微窗口无法通过 DB CAS 消除(已脱离 DB 调度)。 +**可接受的残余风险**:sblk 双 Slot + WriteSequence 单调递增保证了即使并发写入,读取侧永远选择更高 SeqNum 的 Slot,数据最终一致。非 sblk 存储(local/NFS)使用 atomic write(tmp+fsync+rename),最后一次 rename 覆盖前者,同样最终一致。 + +### MN 加入 + +``` +T0: MN-A 独自运行,处理所有 dirty +T1: MN-B 加入 → Poller 启动 +T6: MN-A Poller: claim 3 rows → flush + MN-B Poller: claim 2 rows → flush + → 自然分摊 (Y) +``` + +--- + +# 8. 管理平面恢复策略 + +恢复策略表: + +| 触发源 | 检测方式 | 管理平面行为 | +|--------|---------|-------------| +| 刷写达到重试上限 | `onFlushFailure()` | 告警日志 + 删除 dirty 行(下次 API 自动重试) | +| read 返回 NEED_REPAIR | 巡检/读取时 | `RepairMetadataMsg`(4KB Header 写) | +| read 返回 CORRUPTED | 巡检/读取时 | `markDirty(vmUuid)`(全量重写) | +| read 返回 STORAGE_CHANGE_INCOMPLETE | 巡检/读取时 | `markDirty(vmUuid)` | +| VG 空间不足 | Agent 返回错误码 | 告警 + 退避 + 巡检重试 | +| 注册崩溃残留 | MN 启动/定时扫描 | Saga 回滚(5 条件判断) | +| 存储迁移失败 | 迁移 post-hook(`afterMigrateVmStorageFailed`) | 告警 + `markDirty(vmUuid, storageStructureChange=true)` 自愈。`storageStructureChange=true` 确保下轮 Poller 刷写时 OP type=2 (STORAGE_CHANGE),触发 sblk Agent 端重新定位 Slot。失败回滚同时执行 `deleteMetadata(targetPsUuid, vmUuid)` 清理目标端残留 + `nextRetryTime=NULL` 恢复 Poller。详见 [Part 1c §1.4](vm-metadata-01c-存储层与模板虚拟机.md#14-元数据生命周期) 失败回滚策略 | +| VM 销毁残留 | 销毁 post-hook + 巡检 | 孤儿 LV 检测 + 运维清理 | + +## 8.1 重试上限后的恢复策略 + +采用“告警 + 下次 API 触发自动重试”的简化策略,移除 MetadataStaleEvent → recovery cycle 机制,避免无限重试循环。 + +当 `retryCount >= maxRetry`(默认 5 次,约 5 分钟)时: + +1. **告警**:ERROR 日志记录 vmUuid + 失败原因 + 重试次数 +2. **删除 dirty 行**:放弃本轮重试 +3. **自然恢复**:下次该 VM 的 `@MetadataImpact` API 成功 → `markDirty()` → 全新重试(retryCount=0) + +**为什么不需要 MetadataStaleEvent 恢复机制**: + +| 方面 | 旧方案(recovery cycle) | 新方案(告警 + API 重试) | +|------|--------------------------|---------------------------| +| 复杂度 | 需 ResourceConfig 持久化 cycle 计数 + 优先队列 + 定时任务 | 无额外代码 | +| 无限循环风险 | 需 cycle 上限 + permanently stale 标记 | 不存在(只在 API 触发时重试) | +| 恢复时机 | 固定延迟 5 分钟 | 自然发生(下次 API 时) | +| PS 持续故障 | cycle 耗尽后 permanently stale | 每次 API 都重试一轮(5 次退避),不会无限堆积 | + +路径指纹巡检作为兗底方案:发现路径漂移时调用 `markDirty()` 触发全新刷写(见 §8.2)。 + +## 8.2 路径指纹巡检 — 轻量级漂移检测 + +### 8.2.1 问题:为什么不能读存储比对 + +原方案"周期性全量比对 DB vs 存储元数据"需要 agent 调用读取存储上的 sblk 文件、解码、反序列化,对每个 VM 都是一次 I/O 操作。对于大规模环境(数千 VM),这个开销不可接受。 + +### 8.2.2 思路:写时记录路径快照,读时纯 DB 比对 + +每次 Poller 刷写成功后,将本次构建元数据时用到的**所有 Volume 和 Snapshot 的 installPath** 记录到 DB。一个独立的周期巡检任务从 DB 查询当前路径,与记录的快照比对——**整个过程零存储 I/O**。 + +### 8.2.3 路径指纹结构 + +```java +@Entity +@Table(name = "VmMetadataPathFingerprintVO") +public class VmMetadataPathFingerprintVO { + @Id + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ReferenceOption.CASCADE) + private String vmInstanceUuid; // PK, FK → VmInstanceEO (CASCADE DELETE) + + @Column + @Lob + private String pathSnapshot; // 上次刷写时的路径列表(JSON) + + @Column + private Timestamp lastFlushTime; // 记录时间 + + @Column + private boolean lastFlushFailed; // H2/M1 修复:重试耗尽时置 true,MetadataStaleRecoveryTask 重新入队后置 false + + @Column + private int staleRecoveryCount; // Q27 熔断:MetadataStaleRecoveryTask 累计重入队次数,达到上限后停止自动恢复 +} +``` + +**`lastFlushFailed` 字段说明(M1 修复)**: +- **写入时机**:Poller `onFlushFailure()` 中,当 `retryCount >= maxRetry` 时,在删除 dirty 行之前设置 `lastFlushFailed = true` +- **清除时机**:`MetadataStaleRecoveryTask`(见 [Part 2 §4.8](vm-metadata-02-脏标记与Poller.md#48-stale-恢复任务h2-修复))扫描到该行后调用 `markDirty()` 并重置为 `false` +- **默认值**:`false`(正常刷写成功时不修改此字段) +- **与 §8.1 的关系**:§8.1 的"告警 + 删除 dirty 行 + 下次 API 自动重试"策略保持不变。`lastFlushFailed` 作为补充标记,使得即使没有后续 API 触发,`MetadataStaleRecoveryTask` 也能在 30 分钟内自动发现并重新入队 + +**无限慢重试回路的熔断机制**: +当 PS 长期不可达时,`lastFlushFailed=true → StaleRecoveryTask markDirty → Poller 5 次重试 → 再次 lastFlushFailed=true → 30min 后再来` 形成无限慢循环。 +**熔断设计**:在 `VmMetadataPathFingerprintVO` 增加 `staleRecoveryCount INT DEFAULT 0` 字段,每次 `MetadataStaleRecoveryTask` 重新入队时递增。 +当 `staleRecoveryCount >= vm.metadata.staleRecovery.maxCycles`(默认 10,即约 5 小时)时,置 `lastFlushFailed=false`(停止自动重入队),并记录 WARN 日志: +`"VM [{}] metadata stale recovery exceeded {} cycles, entering permanent-stale. Use APIUpdateVmMetadataMsg to manually trigger."` +管理员可通过 `APIUpdateVmMetadataMsg` 手动触发刷写,该 API 的 `markDirty` 调用会重置 `staleRecoveryCount=0`。 +这避免了对永久不可达 PS 的无限资源消耗,同时保留了手动恢复能力。 + +`pathSnapshot` 格式(JSON,便于调试和日志输出): + +```json +{ + "volumes": [ + {"uuid": "vol-aaa", "installPath": "/dev/vg/vol-aaa"}, + {"uuid": "vol-bbb", "installPath": "/dev/vg/vol-bbb"} + ], + "snapshots": [ + {"uuid": "sp-001", "installPath": "/dev/vg/sp-001"}, + {"uuid": "sp-002", "installPath": "/dev/vg/sp-002"} + ] +} +``` + +列表按 uuid 排序,确保同样的拓扑总是产生相同的 JSON,便于字符串直接比对。 + +**JSON 字段序确定性保证**:`buildPathJson()` 使用 Gson 序列化简单内部 POJO(仅含 `uuid` 和 `installPath` 两个 String 字段),Gson 按 Java 字段声明顺序输出(非 `@SerializedName` alphabetical),声明顺序在编译后固定。列表层面按 `uuid ASC` 排序。两层确定性保证——字段声明顺序 + 列表排序——确保相同拓扑始终产生 byte-identical JSON。 + +### 8.2.4 写入时机 + +Poller 刷写成功 → `deleteRow()` 前,调用 `savePathFingerprint(vmUuid)`: + +```java +private void savePathFingerprint(String vmUuid) { + List volumes = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .orderBy(VolumeVO_.uuid, SimpleQuery.Od.ASC).list(); + List snapshots = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.volumeUuid, volumes.stream().map(VolumeVO::getUuid).collect(toList())) + .orderBy(VolumeSnapshotVO_.uuid, SimpleQuery.Od.ASC).list(); + + VmMetadataPathFingerprintVO fp = new VmMetadataPathFingerprintVO(); + fp.setVmInstanceUuid(vmUuid); + fp.setPathSnapshot(buildPathJson(volumes, snapshots)); + fp.setLastFlushTime(new Timestamp(System.currentTimeMillis())); + dbf.insertOrUpdate(fp); +} +``` + +### 8.2.5 巡检 PeriodicTask(Keyset 分页) + +```java +public class MetadataPathDriftDetector implements PeriodicTask { + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_PATH_CHECK_INTERVAL.value(Long.class); + // 默认 300 秒(5 分钟) + } + + @Override + public void run() { + int batchSize = VmGlobalConfig.VM_METADATA_PATH_CHECK_BATCH_SIZE.value(Integer.class); // default 500 + String lastUuid = ""; + while (true) { + List batch = SQL.New( + "select fp from VmMetadataPathFingerprintVO fp where fp.vmInstanceUuid > :lastUuid order by fp.vmInstanceUuid asc", + VmMetadataPathFingerprintVO.class) + .param("lastUuid", lastUuid) + .limit(batchSize) + .list(); + if (batch.isEmpty()) { + break; + } + + for (VmMetadataPathFingerprintVO fp : batch) { + String currentSnapshot = buildCurrentPathSnapshot(fp.getVmInstanceUuid()); + if (!fp.getPathSnapshot().equals(currentSnapshot)) { + logger.warn("path drift detected for VM [{}], recorded: {}, current: {}", + fp.getVmInstanceUuid(), fp.getPathSnapshot(), currentSnapshot); + markDirty(fp.getVmInstanceUuid()); + } + } + + lastUuid = batch.get(batch.size() - 1).getVmInstanceUuid(); + } + } +} +``` + +**设计要求**:禁止 `dbf.listAll(VmMetadataPathFingerprintVO.class)` 全量加载。大规模环境必须使用 keyset 分页(`vmInstanceUuid > lastUuid`,因 PK 为 `vmInstanceUuid` 而非自增 `id`)。 + +**keyset 分页与非事务性间隙**:INSERT IGNORE 和后续 UUID 分页查询不在同一事务中,期间可能有新 VM 创建或旧 VM 销毁。这是可接受的:新 VM 由下轮巡检覆盖;销毁的 VM 因 FK CASCADE 自动清理 dirty 行和 fingerprint 行。不需要额外处理。 + +### 8.2.6 对比原方案 + +| | 原方案(读存储比对) | 路径指纹巡检 | +|---|---|---| +| I/O 开销 | 每 VM 一次 agent 调用读 sblk | **零存储 I/O**,纯 SQL | +| 可运行频率 | 分钟级(受 agent 吞吐限制) | 秒级(仅 DB 查询) | +| 检测范围 | 完整内容(含格式/编码差异噪声) | 仅存储拓扑路径变更(精准) | +| 首次可用 | 需 VM 已写过元数据 | 同左(需至少一次刷写记录指纹) | +| 存储拓扑变更 | 100% 检测 | 100% 检测(路径覆盖所有拓扑变更) | +| 非拓扑字段变更 | 可检测(如 size/description) | **不检测**(这些已被 `@MetadataImpact` API 覆盖) | +| 调试友好 | 需读存储 + 解码 | JSON 路径列表直接可读,drift 时日志输出新旧对比 | + +### 8.2.7 边界条件 + +| 场景 | 处理 | +|------|------| +| VM 从未刷写过元数据 | 无指纹记录 → 巡检跳过 | +| VM 已销毁 | FK CASCADE → 指纹记录自动删除 | +| 刷写成功但保存指纹前 MN 崩溃 | 指纹仍是旧的 → 下轮巡检发现 drift → markDirty → 重新刷写 + 更新指纹 | +| markDirty 已调用但尚未刷写 | 巡检发现 drift → 再次 markDirty → 幂等(dirty 行已存在,UPSERT 无副作用) | +| 并发刷写 + 巡检 | 巡检 markDirty 后 Poller 刷写覆盖 → 下轮巡检指纹一致 → 收敛 | + +## 8.3 VM 销毁时的元数据清理 + +在 `ExpungeVmInstanceFlow` 链中增加 `NoRollbackFlow` step:查找根卷所在 PS → `metadataStorageHandler.deleteMetadata()` → **best-effort**,失败仅 WARN 日志,不阻塞 VM 物理清除。 + +**删除时机说明(讨论 Δ-5)**:元数据文件的删除发生在 Expunge(物理删除)阶段而非 Destroy(软删除)阶段。Destroy 时 VM 可通过 Recover 恢复,删除元数据将导致恢复后无法自愈。Expunge 是不可逆操作,此时删除是安全的。 + +dirty 行的清理由 FK CASCADE 自动完成(VM 物理删除 → VmInstanceEO 删除 → dirty 行级联删除)。 + +**VmInstanceEO 软删除时序**:ZStack 的 VM 删除分两阶段: +1. 软删除:`VmInstanceVO` 的 `@SoftDeletionCascade` 将记录从 `VmInstanceVO` 视图移除,但底层 `VmInstanceEO` 行仍存在。此时 FK CASCADE **不触发**,dirty 行保留。若 Poller 此时认领该 VM,Part 2 §4.3 的 Destroyed 状态过滤会跳过。 +2. 物理删除:`GarbageCollectorVO` 驱动的清理任务在软删除后数分钟到数小时执行 `DELETE FROM VmInstanceEO WHERE uuid=?`,此时 FK CASCADE 触发,dirty 行 + fingerprint 行被级联删除。 +在软删除到物理删除的窗口内,dirty 行存在但被 Poller 的 Destroyed 过滤跳过,不会产生多余 flush 操作。物理删除后所有关联行自动清理。整条链路无需额外处理。 + +## 8.4 孤儿元数据检测与清理 + +### 8.4.1 孤儿产生场景 + +| 场景 | 原因 | 孤儿位置 | +|------|------|----------| +| VM 销毁时 `deleteMetadata` 失败 | Agent 超时/PS 不可用 | 元数据残留在 VM 原根盘所在 PS | +| 存储迁移崩溃([Part 1c §1.4](vm-metadata-01c-存储层与模板虚拟机.md#14-元数据生命周期) SM-02) | MN 在 Step 2 成功后、Step 8 前崩溃,且未触发回滚 | 目标 PS 上有孤儿元数据 | +| 存储迁移成功但 Step 8 清理失败 | 源 PS 删除元数据失败 | 源 PS 上残留旧元数据 | + +### 8.4.2 检测机制 — MetadataOrphanDetector + +独立 PeriodicTask,低频运行(默认每小时一次),扫描存储上的元数据并比对 DB 状态: + +```java +public class MetadataOrphanDetector implements PeriodicTask { + @Override + public long getInterval() { + return VmGlobalConfig.VM_METADATA_ORPHAN_CHECK_INTERVAL.value(Long.class); + // 默认 3600 秒(1 小时) + } + + @Override + public void run() { + // 逐 PS 扫描,复用 Scan API 的 Agent 调用 + List allPs = Q.New(PrimaryStorageVO.class) + .in(PrimaryStorageVO_.type, List.of("SharedBlock", "LocalStorage", "NFS")) + .eq(PrimaryStorageVO_.state, PrimaryStorageState.Enabled) + .list(); + + for (PrimaryStorageVO ps : allPs) { + detectOrphansOnPs(ps); + } + } + + private void detectOrphansOnPs(PrimaryStorageVO ps) { + // 1. Agent 扫描该 PS 上所有元数据条目(轻量:仅返回 vmUuid 列表) + List vmUuidsOnStorage = metadataStorageHandler.scanMetadataVmUuids(ps.getUuid()); + + for (String vmUuid : vmUuidsOnStorage) { + // 2. 检查 VM 是否存在 + VmInstanceVO vm = dbf.findByUuid(vmUuid, VmInstanceVO.class); + if (vm == null) { + // VM 已销毁 → 确认孤儿 + reportOrphan(ps.getUuid(), vmUuid, "VM_DELETED"); + continue; + } + + // 3. 检查 VM 根盘是否在此 PS 上 + String rootPsUuid = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .eq(VolumeVO_.type, VolumeType.Root) + .select(VolumeVO_.primaryStorageUuid) + .findValue(); + + if (rootPsUuid != null && !rootPsUuid.equals(ps.getUuid())) { + // 根盘在其他 PS → 此 PS 上的元数据是迁移残留 + reportOrphan(ps.getUuid(), vmUuid, "ROOT_ON_OTHER_PS"); + } + } + } + + private void reportOrphan(String psUuid, String vmUuid, String reason) { + logger.warn("orphan metadata detected: ps={}, vm={}, reason={}", psUuid, vmUuid, reason); + // 记录审计日志,不自动删除(安全起见) + } +} +``` + +### 8.4.3 清理策略 + +孤儿元数据**仅报告不自动删除**,原因: +- 迁移崩溃后 MN 重启,`recoverStalledMigrationPauses()`([Part 1c §1.6](vm-metadata-01c-存储层与模板虚拟机.md#16-存储迁移-poller-暂停的崩溃恢复))重置 `nextRetryTime`,Poller 从 DB 全量重建写入正确 PS,迁移残留自然成为孤儿 +- 自动删除有误删风险(如扫描与 DB 查询之间 Root Volume 正在迁移) + +运维可通过以下方式按需清理: +1. `APICleanupVmInstanceMetadataMsg`([Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据))指定 `vmUuids` + `primaryStorageUuids` 精确清理 +2. sblk:`lvremove {vg}/{vm_uuid}_vmmeta` +3. local/NFS:`rm {mountPath}/.zstack-vm-metadata/{vm_uuid}.json` + +### 8.4.4 与 §8.3 的关系 + +| 场景 | §8.3 处理 | §8.4 兜底 | +|------|-----------|-----------| +| VM 销毁 deleteMetadata 成功 | (Y) 清理完成 | 不会检测到孤儿 | +| VM 销毁 deleteMetadata 失败 | (!) WARN 日志 | 1 小时后检测到 `VM_DELETED` 孤儿 | +| 迁移崩溃残留 | 不涉及(VM 未销毁) | 1 小时后检测到 `ROOT_ON_OTHER_PS` 孤儿 | + +## 8.5 主存储卸载/重新挂载时的元数据行为 + +| 阶段 | 行为 | +|------|------| +| PS 卸载(Detach) | Poller flush 失败(Agent 不可达),dirty 行进入 retry→stale 周期。PathFingerprint 的 `lastFlushFailed=true`。Poller 不主动清理 dirty 行,保留供后续恢复。 | +| PS 保持卸载 | StaleRecoveryTask 周期性重入队 markDirty → 5 次重试失败 → 再次 stale → 最终触发 Q27 熔断,停止自动恢复(约 5 小时后)。WARN 日志提示管理员。 | +| PS 重新挂载(Reattach) | 下一次 API 触发的 `markDirty` 或管理员手动 `APIUpdateVmMetadataMsg` 重新入队。若已熔断,`APIUpdateVmMetadataMsg` 重置 `staleRecoveryCount=0`。Poller 正常 flush 恢复。 | +| 无需特殊处理的原因 | dirty 行和 fingerprint 行在 DB 中持久化,PS 卸载不影响 DB 状态。恢复后 Poller 从 DB 全量读取构建 payload,确保元数据完整。 | + +--- + +# 9. 升级后全量刷新 + +## 9.1 触发条件 + +在 `managementNodeReady()` 回调中执行: + +1. 查询所有在线 `ManagementNodeVO`,收集 version 集合 +2. 若存在多个不同版本(滚动升级中)→ 跳过 +3. 版本唯一且与 `lastRefreshVersion`(GlobalConfig 持久化)不同 → 提交延迟 10 分钟的定时任务 +4. 10 分钟后再次检查所有 MN 版本是否一致 → 一致则执行全量刷新,不一致则跳过 +5. **recent-nodeLeft 检查(M3 修复)**:执行全量刷新前,检查最近 15 分钟内是否有 `nodeLeft` 事件。若有,说明可能仍在滚动升级过程中(旧 MN 刚下线),延迟 10 分钟后重新从步骤 1 开始检查 + +**M3 修复说明**:滚动升级的典型模式是"停旧 MN → 升级 → 启新 MN"。在这个过程中,可能出现短暂的"版本唯一"假象: +- T0:MN-A(v2) 启动,MN-B(v1) 尚未下线 → 版本不同 → 步骤 2 跳过 (Y) +- T1:MN-B(v1) 下线 → `nodeLeft` 事件 +- T2:MN-A(v2) 是唯一 MN → 版本唯一 → 步骤 3 匹配 → 提交延迟任务 +- T3(10min 后):步骤 4 检查 → 仍只有 MN-A → 版本一致 → 触发全量刷新 +- T4(但 T3+5min 后):MN-B(v2) 上线 → **此时已不需要再次全量刷新** + +问题在步骤 T3:虽然版本一致,但 MN-B 还未上线。在 MN-B 上线前执行全量刷新是正确的(它也会处理),但若 MN-B 的 `managementNodeReady()` 也触发同样逻辑,会导致**两次全量刷新**。通过 `lastRefreshVersion` 检查可避免重复(步骤 3 的 `lastRefreshVersion != currentVersion` 条件),所以实际安全。 + +但真正的风险是:升级窗口内旧版 MN 的元数据刷写可能使用旧 schema,全量刷新应确保**所有 MN 都已升级完成**。`recent-nodeLeft` 检查补充了这一保证。 + +**延迟 10 分钟的原因**:滚动升级期间,第一个 MN 升级完成时可能短暂出现"版本唯一"假象(旧 MN 尚未恢复上线)。 + +## 9.2 刷新执行(简化,无 LongJob) + +不需要 LongJob。直接批量 markDirty,Poller 自动处理。 + +```java +private void submitFullRefresh(String currentVersion) { + logger.info("metadata full refresh: starting for version {}", currentVersion); + + // Q24 修复:按 C-DM-01 要求使用 INSERT IGNORE + UPDATE 两步,不使用 ON DUPLICATE KEY + // 同时使用 keyset 分页替代 OFFSET,避免大数据集性能退化 + int batchSize = VmGlobalConfig.VM_METADATA_UPGRADE_REFRESH_BATCH_SIZE.value(Integer.class); // default 1000 + String lastUuid = ""; + int totalProcessed = 0; + + while (true) { + // Step 1: INSERT IGNORE — 为尚无 dirty 行的 VM 创建新行 + // storageStructureChange=true(C-SC-07:升级后无法判断存储拓扑是否变化,保守使用 STORAGE 级别) + int inserted = SQL.New( + "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "SELECT v.uuid, 1, 1 FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + // Step 2: UPDATE — 已有 dirty 行的 VM 递增 dirtyVersion + 升级 storageStructureChange + SQL.New( + "UPDATE VmMetadataDirtyVO d " + + "INNER JOIN VmInstanceVO v ON d.vmInstanceUuid = v.uuid " + + "SET d.dirtyVersion = d.dirtyVersion + 1, " + + " d.storageStructureChange = 1 " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + // 更新 lastUuid 用于 keyset 分页 + List batch = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC LIMIT :batchSize", String.class) + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .list(); + + if (batch.isEmpty()) { + break; + } + + totalProcessed += batch.size(); + lastUuid = batch.get(batch.size() - 1); + } + + logger.info("metadata full refresh: {} VMs processed for version {}", totalProcessed, currentVersion); + + // Poller 自动分批处理,ChainTask 自动限流 + + // 更新 lastRefreshVersion — 必须在全量刷新完成后写入(讨论 Δ-8) + // 不得在刷新开始前写入:若刷新过程中 MN 崩溃,提前写入会导致重启后 + // lastRefreshVersion 已等于 currentVersion,跳过本次刷新,遗留未处理的 VM。 + // 写在完成后:崩溃重启 → lastRefreshVersion 仍为旧值 → 重新触发全量刷新 → 幂等安全。 + VmGlobalConfig.VM_METADATA_LAST_REFRESH_VERSION.updateValue(currentVersion); +} +``` + +**说明**:原实现使用 `INSERT ... ON DUPLICATE KEY UPDATE` 单条 SQL,与 C-DM-01 约束(禁用 ON DUPLICATE KEY,避免 Galera 死锁)不一致。改为 `INSERT IGNORE + UPDATE` 两步语义,与 `markDirty()` 保持统一。同时将 `OFFSET` 分页改为 keyset 分页(`uuid > :lastUuid`),与 §9a.1 和 §8.2.5 保持一致,避免大数据集性能退化。 + +**storageStructureChange=true 已修正**:与 C-SC-07 约束对齐——升级全量刷新场景中无法判断存储拓扑是否变化,保守使用 `storageStructureChange=true`(原实现误设为 0)。 + +**为什么用分批批量 SQL 替代逐个 markDirty**:万级 VM 环境中,逐个 INSERT 产生万级 SQL 语句;单条超大批量 SQL 又可能超时。按 1000 行分批可在吞吐与稳定性间平衡。 + +--- + +# 9a. 功能开关切换处理 + +## 9a.1 `false → true`(启用)— 分批全量初始化 + +通过 `GlobalConfig.installUpdateExtension` 监听 `vm.metadata.enabled` 变更。检测到 `false → true` 时,提交分批初始化任务,为所有尚无元数据(无 dirty 行也无 PathFingerprint 记录)的 UserVm 创建 dirty 行。 + +**核心设计:防止读写风暴** + +与升级全量刷新(§9.2)不同,开关启用可能在业务高峰时执行。直接批量 INSERT 大量 dirty 行后 Poller 瞬间看到全部可认领行,可能引发存储 IO 风暴。因此引入**批间延迟**: + +```java +private void submitBatchInitialization() { + thdf.submit(new Task(null) { + @Override + public Void call() { + if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { + // 延迟执行前再次检查,防止快速 toggle 后仍执行初始化 + logger.info("vm.metadata.enabled toggled back to false before initialization, skip"); + return null; + } + + int batchSize = VmGlobalConfig.VM_METADATA_INIT_BATCH_SIZE.value(Integer.class); // default 200 + long batchDelaySec = VmGlobalConfig.VM_METADATA_INIT_BATCH_DELAY_SEC.value(Long.class); // default 5 + String lastUuid = ""; + int totalInitialized = 0; + + while (true) { + // 每轮检查开关状态,若已关闭则中止 + if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { + logger.info("vm.metadata.enabled disabled during initialization, abort. initialized={}", + totalInitialized); + break; + } + + // Keyset 分页查询尚无 dirty 行的 UserVm + int initialized = SQL.New( + "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + + "SELECT v.uuid, 1, 0 FROM VmInstanceVO v " + + "LEFT JOIN VmMetadataDirtyVO d ON v.uuid = d.vmInstanceUuid " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid AND d.vmInstanceUuid IS NULL " + + "ORDER BY v.uuid ASC LIMIT :batchSize") + .param("lastUuid", lastUuid) + .param("batchSize", batchSize) + .execute(); + + // Q29 修复:移除 `if (initialized == 0) break;`——当本批所有 VM 都已有 dirty 行时 + // INSERT IGNORE affected_rows=0,但后续批次可能还有未初始化的 VM。 + // 终止条件改为 batchUuids.isEmpty()(见下方),确保真正遍历完全部 VM。 + + totalInitialized += initialized; + + // 更新 lastUuid 用于 keyset 分页 + // Q29 — lastUuid 必须独立推进:当 INSERT IGNORE affected_rows=0(本批 VM 都已有 dirty 行) + // 时 initialized==0,但 while 循环不能终止——后续批次可能还有未初始化的 VM。 + // lastUuid 基于 VmInstanceVO 全量 UUID 推进,而非 INSERT 结果。 + List batchUuids = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + + "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + + "ORDER BY v.uuid ASC", String.class) + .param("lastUuid", lastUuid) + .limit(batchSize) + .list(); + + if (batchUuids.isEmpty()) { + break; // 真正遍历完所有 VM + } + lastUuid = batchUuids.get(batchUuids.size() - 1); + + logger.info("metadata initialization batch completed: {} VMs in this batch, {} total", + initialized, totalInitialized); + + // 批间延迟:等待 Poller 消化已有 dirty 行,避免瞬间堆积 + if (batchDelaySec > 0) { + try { + TimeUnit.SECONDS.sleep(batchDelaySec); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("metadata initialization interrupted"); + break; + } + } + } + + logger.info("metadata initialization complete: {} VMs total", totalInitialized); + return null; + } + }, Duration.ofSeconds(30)); // 延迟 30s 启动,等待 Poller 就绪 +} +``` + +**关键设计点**: + +| 设计点 | 决策 | 原因 | +|--------|------|------| +| 使用 `INSERT IGNORE` | 跳过已有 dirty 行的 VM | 幂等:重复触发不产生副作用 | +| `LEFT JOIN` 排除已有 dirty 行 | 仅为从未标脏的 VM 初始化 | 避免对已有 Poller 处理中的 VM 产生干扰 | +| Keyset 分页(`uuid > lastUuid`) | 避免 `OFFSET` 在大数据集上的性能退化 | 与路径指纹巡检(§8.2.5)保持一致 | +| 批间延迟(默认 5s) | 给 Poller 消化已提交 dirty 行的时间窗口 | 防止 dirty 行瞬间堆积万级,触发存储 IO 风暴 | +| 每轮重新检查 `vm.metadata.enabled` | 快速 toggle(开→关→开)场景下及时中止 | 防御性设计 | +| 延迟 30s 启动 | 等 Poller、ChainTask 线程池初始化完成 | `false→true` 可能在 MN 启动时通过 GlobalConfig 变更触发 | +| `storageStructureChange=0` | 首次初始化不涉及存储拓扑变更 | 用 CONFIG 级别即可,不需 STORAGE 级重建 | + +**初始化进度可观测**: + +- 日志输出每批和总计数 +- Poller 的 `vm_metadata_dirty_queue_size` Gauge(§14)自然反映待处理积压量 +- 运维可通过 `SELECT COUNT(*) FROM VmMetadataDirtyVO WHERE managementNodeUuid IS NULL` 查看剩余量 + +**与 §9.2 升级全量刷新的关系**: + +| 维度 | §9 升级全量刷新 | §9a 开关启用初始化 | +|------|----------------|-------------------| +| 触发时机 | MN 升级后自动触发 | GlobalConfig 从 false 切换到 true | +| 涉及 VM 范围 | 所有 UserVm(含已有 dirty 行的) | 仅尚无 dirty 行的 UserVm | +| SQL 策略 | `INSERT ... ON DUPLICATE KEY UPDATE`(保证已有行也递增版本) | `INSERT IGNORE ... LEFT JOIN`(仅初始化新行) | +| 批间延迟 | 无(升级窗口通常业务量低) | 有(默认 5s,防止高峰期风暴) | +| 去重保护 | `lastRefreshVersion` 检查避免重复触发 | `LEFT JOIN` + `INSERT IGNORE` 天然幂等 | + +## 9a.2 `true → false`(禁用)— 保留已有元数据,按需清理 + +关闭 `vm.metadata.enabled` 后: + +1. **Poller 自动停止处理**:`markDirty()` 和 `triggerFlushForVm()` 的前置检查直接 return,不再产生新的标脏和刷写 +2. **清理 PathFingerprint 记录(讨论 Δ-10)**:异步批量删除所有 `VmMetadataPathFingerprintVO` 行。原因:功能关闭期间存储拓扑可能发生变更(卷迁移、快照删除等),重新启用时旧指纹与实际拓扑不一致,会导致路径巡检(§8.2)产生大量误报 drift。清理采用 keyset 分页异步删除(每批 1000 行),不阻塞 GlobalConfig 变更回调。dirty 行的 FK CASCADE 不受影响。 +3. **已有 dirty 行保留**:不主动清理 `VmMetadataDirtyVO` 表中的残留行。原因: + - 若运维快速重新开启(toggle back),残留行可立即被 Poller 消费 + - 若长期关闭,残留行占用 DB 空间可忽略(每行 < 200 bytes) +4. **存储上的元数据文件/LV 保留**:不自动删除已持久化的元数据。理由: + - 防止误操作导致已有容灾数据丢失 + - 元数据文件体积小(通常 < 500KB/VM),即使保留也不构成存储压力 + - 运维需要时可通过 `APICleanupVmInstanceMetadataMsg` 按需清理 + +**`APICleanupVmInstanceMetadataMsg`** 详见 [Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据)。核心设计: + +- 可按 `primaryStorageUuids`(指定 PS)或 `vmUuids`(指定 VM)粒度清理 +- 不传参数则清理**所有已停用(`vm.metadata.enabled=false`)时的全量数据** +- 清理操作为 `deleteMetadata` + 删除 `VmMetadataPathFingerprintVO` + 删除残留 `VmMetadataDirtyVO` +- 清理操作幂等:重复调用不报错 + +**安全约束**:`APICleanupVmInstanceMetadataMsg` 在 `vm.metadata.enabled=true` 时**拒绝执行**(错误码 `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`),防止在功能启用状态下误清理正在使用的元数据。仅当功能关闭后才允许执行。 + +--- + +# 10. Payload 大小保护 + +在 `VmInstanceBase.doHandleUpdateVmInstanceMetadata()` 中,`buildVmInstanceMetadata()` 构建 payload 后进行大小检查: + +| 阈值 | 行为 | 说明 | +|------|------|------| +| > 8MB | WARN 日志 | 早期预警,提示运维关注 | +| > 30MB | ERROR + 拒绝写入 + reply 错误 | 保护 sblk LV 空间 | + +正常 VM 的 metadata payload 通常在 10KB~500KB 范围内。超过 8MB 几乎一定表示异常(如快照未清理导致数千条记录)。 + +### 10.0 容量公式与常量(QX-8) + +```java +public final class VmMetadataConstants { + public static final long SBLK_HEADER_SIZE = 4096L; + public static final long SBLK_SLOT_HEADER_SIZE = 36L; + public static final long SBLK_MAX_LV_SIZE = 64L * 1024 * 1024; + + public static long slotCapacity(long lvSize) { + return ((lvSize - SBLK_HEADER_SIZE) / 2 / 4096) * 4096; + } + + public static final long SBLK_MAX_SLOT_CAPACITY = slotCapacity(SBLK_MAX_LV_SIZE); // 33,550,336 + public static final long SBLK_MAX_PAYLOAD_SIZE = SBLK_MAX_SLOT_CAPACITY - SBLK_SLOT_HEADER_SIZE; // 33,550,300 + public static final long PAYLOAD_WARN_THRESHOLD = 8L * 1024 * 1024; + public static final long PAYLOAD_REJECT_THRESHOLD = 30L * 1024 * 1024; +} +``` + +推导:64MB LV 下单 Slot 容量约 32MB;扣除 Slot Header(36 字节:Magic 4B + SeqNum 8B + SlotOffset 8B + SlotCapacity 8B + PayloadLen 8B)后可用 payload 约 31.99MB。30MB 阈值为显式保守余量。 + +### 10.1 Payload 大小与 sblk LV 大小映射 + +| Payload 大小范围 | LV 大小 | 典型场景 | +|-------------------|---------|----------| +| 0 ~ 2MB | 4MB(初始) | 普通 VM,1~5 个卷,少量快照 | +| 2MB ~ 4MB | 8MB | 多卷 VM,数十个快照 | +| 4MB ~ 8MB | 16MB | 大量快照的 VM | +| 8MB ~ 16MB | 32MB | 异常场景(WARN) | +| 16MB ~ 30MB | 64MB(上限) | 极端异常 | +| > 30MB | 拒绝写入 | 拒绝以保护存储 | + +LV 初始 4MB,每次扩容翻倍,最大 64MB。扩容通过 `lvextend` 完成,详见 [Part 4e §2](vm-metadata-04e-sblk运维与IO.md#2-扩容)。 + +### 10.2 写入前运行时容量校验 + +`doFlush()` 必须基于**当前 LV 实际大小**执行动态容量校验,禁止仅依据静态 30MB 阈值: + +```java +long lvSize = sblkAgent.getLvSize(psUuid, vmUuid); +long slotCap = VmMetadataConstants.slotCapacity(lvSize); +long currentPayloadCap = slotCap - VmMetadataConstants.SBLK_SLOT_HEADER_SIZE; + +if (payloadSize > currentPayloadCap) { + sblkAgent.expandLv(psUuid, vmUuid); + long newLvSize = sblkAgent.getLvSize(psUuid, vmUuid); + long newPayloadCap = VmMetadataConstants.slotCapacity(newLvSize) - VmMetadataConstants.SBLK_SLOT_HEADER_SIZE; + if (payloadSize > newPayloadCap) { + throw new PayloadTooLargeException(String.format( + "payload=%d exceeds slot capacity=%d after expand, lvSize=%d", payloadSize, newPayloadCap, newLvSize)); + } +} +``` + +若扩容后仍不足(通常达到 64MB 上限),返回明确错误码 `VM_METADATA_PAYLOAD_TOO_LARGE`,不得将底层 IO 错误透传为通用失败。 + +--- + +# 11. 潜在代价与 tradeoff + +| 代价 | 说明 | 缓解 | +|------|------|------| +| Poller 空转 | 无 dirty 行时每 5s 执行一次 SELECT → 0 rows | 开销极小(一次空查询 <1ms),可接受 | +| 双 MN 负载不均 | 两个 Poller 竞争认领,不保证 50/50 | 最终一致性保证所有行都会被处理 | +| 新增一张 DB 表 | VmMetadataDirtyVO | 结构简单,维护成本低 | +| 退避期间 Poller 查到但跳过 | nextRetryTime 尚未到 → WHERE 条件排除 | 索引命中,开销可忽略 | + +--- + +# 12. 开发约束清单 + +## 12.1 API 标注约束 + +| # | 约束 | 原因 | 违反后果 | +|---|------|------|----------| +| A1 | 新增影响 VM 元数据的 API **必须**标注 `@MetadataImpact(Impact.CONFIG)` 或 `@MetadataImpact(Impact.STORAGE)` | 拦截器仅扫描带注解的 API 类 | 该 API 的变更不会触发元数据更新,存储侧数据过期 | +| A2 | 明确不影响元数据的 API **应当**标注 `@MetadataImpact(Impact.NONE)` | Opt-out 显式声明,利于 Code Review 审查覆盖率 | 无功能影响,但降低可审计性 | +| A3 | 涉及存储拓扑变更的 API(快照/迁移/删盘)必须使用 `Impact.STORAGE`,不可用 `Impact.CONFIG` | STORAGE 下发 OP type=2 通知 Agent 处理存储拓扑变更 | Agent 不执行存储拓扑处理,sblk 场景可能数据不一致 | +| A4 | `updateOnFailure=true` 仅用于可能部分成功的 API(如批量操作) | 默认 false:失败跳过;设为 true 时失败也 markDirty | 滥用会导致失败 API 也触发无意义的全量刷写 | + +## 12.2 VM UUID 解析约束 + +| # | 约束 | 原因 | 违反后果 | +|---|------|------|----------| +| B1 | 非 VM 直接 API(如 Volume/Nic/Tag API)必须有 `VmUuidFromApiResolver` 能够处理 | 默认 Resolver 链仅覆盖 `VmInstanceMessage`/`VolumeMessage`/Tag API + 反射兜底 | 相关 VM 不会被 markDirty,元数据不更新 | +| B2 | Resolver 的 `resolveVmUuids()` 必须在 **API 执行前**调用(`beforeDeliveryMessage` 阶段) | API 执行后资源可能已删除(如 APIDeleteVolumeMsg → VolumeVO 不存在) | 无法查到关联 VM,markDirty 丢失 | +| B3 | 新增资源类型关联 VM 时,需在 `ResourceBasedVmUuidFromApiResolver.resolveByResourceType()` 中补充映射 | 当前仅覆盖 VmInstanceVO/VolumeVO/VmNicVO/VolumeSnapshotVO | Tag 操作目标为新资源类型时不触发元数据更新 | + +## 12.3 元数据构建约束 + +| # | 约束 | 原因 | 违反后果 | +|---|------|------|----------| +| C1 | `buildVmInstanceMetadata()` 必须保留在 `VmMetadataBuilder` 中并标注 `@Transactional(readOnly=true)` | 6+ 条 SELECT 需在同一 REPEATABLE READ 快照内执行 | 读到跨快照不一致数据(如 Volume 存在但其 Snapshot 已被并发删除) | +| C2 | 新增元数据字段时,需同步更新 `VmInstanceMetadataDTO` 和 `VmMetadataBuilder` | DTO 是 payload 的唯一 schema 定义 | 字段不在 DTO 中则不会序列化到 payload | +| C3 | `ResourceMetadata` 中 `systemTags`/`resourceConfigs` 字段必须为 `String`(Base64 编码),不是 `List` | 编码管线:VO 列表 → JSON 序列化 → Base64 → 单 String | 类型不匹配导致序列化异常 | + +## 12.4 标脏与刷写约束 + +| # | 约束 | 原因 | 违反后果 | +|---|------|------|----------| +| D1 | 修改 VM 存储拓扑的**内部消息** handler 必须手动调用 `markDirty()` | 非 API 操作不经过 `VmMetadataUpdateInterceptor` | 变更后元数据不更新 | +| D2 | Handler 端写入失败时**不得**调用 `markDirty()`,必须 reply error 由上层重试 | Dirty 行已存在且由 Poller 管理 retryCount 和退避 | markDirty 重置 retryCount,绕过退避机制,可能无限快速重试 | + +**D2 例外:存储迁移失败**(§8 恢复策略表)中 `afterMigrateVmStorageFailed` 调用 `markDirty(vmUuid, storageStructureChange=true)` 不违反 D2。原因:存储迁移失败的回滚会改变 installPath(从 target PS 回退到 source PS),旧 dirty 行中缓存的 installPath 指向已回滚的 target 路径,已不正确。此时必须重新 markDirty 以反映回滚后的 source-side 拓扑。这是 D2 的唯一显式例外。同时,回滚操作 `deleteMetadata(targetPsUuid) + nextRetryTime=NULL` 确保不会产生无限快速重试(retryCount 因是新 dirty 行而从 0 开始,退避机制正常生效)。 +| D3 | Agent 端写入必须幂等(全量覆盖,不做增量 merge) | 同一 VM 的并发刷写(跨 MN 极端场景)最终应收敛到一致状态 | 增量 merge 可能导致数据残留或顺序依赖 | +| D4 | `exceedMaxPendingCallback` 中**必须**执行 `globalFlushInFlight.decrementAndGet()` + `releaseClaim()` | **讨论 Δ-1 更新**:采用单层 per-VM ChainTask + AtomicInteger 全局计数器后,exceedMaxPendingCallback 表示该 VM 已有 pending 任务排队,当前提交被拒绝。此时必须归还全局计数器配额并释放 DB 认领,让 Poller 下轮重新处理。旧约束(不得释放)基于嵌套 ChainTask 设计,已不适用 | 计数器泄漏导致全局并发配额耗尽,dirty 行被永久锁定 | +| D5 | `markDirty(vmUuid, storageStructureChange)` 中 `storageStructureChange` 必须保持 OR 语义(true 一旦出现即保持 true 至该行删除) | 保守策略确保任一存储拓扑变更最终走 `STORAGE_CHANGE` 写入路径(intentional conservative behavior) | 若改为覆盖语义,可能把真实拓扑变更降级为 CONFIG 路径 | + +#### D1 补充说明 — 内部消息 handler 遗漏 `markDirty()` 的补救 + +**为什么会遗漏**:`@MetadataImpact` 注解 + CI 检查仅覆盖 `APIMessage` 子类。内部消息(如 HA handler、级联删除、定时清理等)不经过 `VmMetadataUpdateInterceptor`,CI 无法自动检测是否遗漏了 `markDirty()` 调用。 + +**补救手段——分三层**: + +| 层次 | 手段 | 时效 | 说明 | +|------|------|------|------| +| 即时修复 | `APIUpdateVmMetadataMsg` | 秒级 | 运维/CLI 手动触发指定 VM 的全量元数据刷新(见 [Part 5 §6.1](vm-metadata-05-API设计.md#61-手动触发元数据更新)) | +| 批量修复 | `APICheckVmInstanceMetadataConsistencyMsg` | 分钟级 | 一致性检查发现 DB 与存储元数据不一致时自动 `markDirty()`(见 [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性)) | +| 长期防御 | 路径指纹巡检 | 分钟级 | 每次刷写成功后记录路径快照,独立 PeriodicTask 纯 DB 比对检测漂移,不一致则自动 `markDirty()`(见 §8.2) | + +**根因修复流程**: + +``` +发现元数据滞后(运维报告 / 一致性检查告警) + │ + ├─ 1. 定位遗漏的内部消息 handler + │ - 查看该 VM 近期操作日志,找到触发存储变更但未更新元数据的内部操作 + │ - 在对应 handler 的成功回调中补充 markDirty(vmUuid) 调用 + │ + ├─ 2. 即时修复受影响的 VM + │ - CLI: `UpdateVmMetadata uuid=` + │ - 或批量: `CheckVmInstanceMetadataConsistency` + │ + └─ 3. 代码评审防护 + - 涉及 Volume/Snapshot/installPath 变更的内部消息 handler 代码评审时 + 必须检查是否调用了 markDirty() + - 评审 checklist 模板中增加 "元数据标脏" 检查项 +``` + +**注意**:即使存在遗漏,全量覆盖写语义保证了补救时的正确性——任何时刻调用 `markDirty()` 都会触发从 DB 重新构建完整元数据并覆盖写入,不存在增量丢失问题。遗漏的影响是元数据**暂时落后于 DB**,而非**永久损坏**。 + +## 12.5 并发与线程约束 + +| # | 约束 | 原因 | 违反后果 | +|---|------|------|----------| +| E1 | `nodeLeft()`/`nodeJoined()` 回调中的 DB 操作必须通过 `thdf.submit()` 异步执行 | 回调在心跳检测线程上,阻塞会影响其他 MN 状态检测 | 心跳超时导致误判 MN 离线 | +| E2 | per-VM ChainTask(`metadata-dirty-flush-vm-{uuid}`)的 `maxPending=1`,不得修改 | 确保同一 VM 最多排队 1 个 pending 任务(+ 1 running) | pending 过多导致重复提交堆积 | +| E3 | 外层全局队列 `syncLevel` 和 Layer 3 per-PS 队列 `syncLevel` 的调整需评估 DB 连接池和 Agent 并发承受力 | 二者嵌套:全局水位 × per-PS 水位 决定实际并发 | 过大导致 DB/Agent 过载,过小导致刷写积压 | + +--- + +# 13. GlobalConfig 配置项汇总 + +| 配置项 | 类型 | 默认值 | 说明 | 章节 | +|--------|------|--------|------|------| +| `vm.metadata.enabled` | Boolean | false | 元数据功能总开关 | §1 | +| `vm.metadata.dirty.pollIntervalSec` | Long | 5 | Poller 轮询间隔(秒),可动态调整 | §4.1 | +| `vm.metadata.dirty.batchSize` | Integer | 50 | 每轮 Poller 最多认领行数 | §4.2 | +| `vm.metadata.maxRetry` | Integer | 5 | 最大重试次数(达上限后告警 + 删除,下次 API 自动重试) | §4.6 | +| `vm.metadata.ps.maxConcurrent` | Integer | 5 | 同一 MN 同一 PS 最大并发写入 | §6.1 | +| `vm.metadata.global.maxConcurrent` | Integer | 10 | 同一 MN 最大并发 VM 更新数 | §6.2 | +| `vm.metadata.pathCheck.intervalSec` | Long | 300 | 路径指纹巡检间隔(秒) | §8.2 | +| `vm.metadata.pathCheck.batchSize` | Integer | 500 | 路径指纹巡检 keyset 分页批次 | §8.2.5 | +| `vm.metadata.upgrade.refreshDelaySec` | Long | 600 | 升级后全量刷新延迟时间(秒),等待滚动升级完成 | §9.1 | +| `vm.metadata.upgrade.refreshBatchSize` | Integer | 1000 | 升级全量刷新分批 SQL 批次大小 | §9.2 | +| `vm.metadata.nodeLeft.delaySec` | Long | 5 | nodeLeft 后延迟接管窗口,降低 zombie MN 竞态 | §7.2 | +| `vm.metadata.staleRecovery.intervalSec` | Long | 1800 | MetadataStaleRecoveryTask 扫描间隔(秒)(H2 修复) | §4.8 (Part 2) | +| `vm.metadata.staleRecovery.batchSize` | Integer | 100 | MetadataStaleRecoveryTask 每批扫描行数(H2 修复) | §4.8 (Part 2) | +| `vm.metadata.staleRecovery.maxCycles` | Integer | 10 | 单 VM 连续 stale recovery 熔断阈值,超过后停止自动恢复 | §8.2.3 | +| `vm.metadata.pendingApi.timeoutMinutes` | Long | 45 | pendingApis 超时清理阈值(分钟)(M4 修复) | §1.7 (Part 1b) | +| `vm.metadata.retry.baseDelaySeconds` | Integer | 10 | 指数退避基础延迟(秒) | §4.6 (Part 2) | +| `vm.metadata.retry.maxExponent` | Integer | 10 | 指数退避最大指数 | §4.6 (Part 2) | +| `vm.metadata.init.batchSize` | Integer | 200 | `false→true` 启用初始化每批 VM 数量 | §9a.1 | +| `vm.metadata.init.batchDelaySec` | Long | 5 | `false→true` 启用初始化批间延迟(秒),防止 IO 风暴 | §9a.1 | +| `vm.metadata.orphanCheck.intervalSec` | Long | 3600 | 孤儿元数据检测间隔(秒) | §8.4.2 | +| `vm.metadata.zombieClaim.thresholdMinutes` | Long | 15 | 僵尸 claim 判定阈值(分钟):`lastClaimTime` 超过此时长的已认领 dirty 行视为僵尸,`cleanupZombieClaims()` 释放其认领 | §4.8 (Part 2), C-CL-02 | +| `vm.metadata.staleClaim.thresholdMinutes` | Long | 30 | `MetadataStaleRecoveryTask` 后台扫描的过期 claim 检测阈值(分钟):`managementNodeUuid IS NOT NULL` 且 `lastClaimTime` 超过此时长的行被强制释放并重新入队。**注意**:此阈值仅用于后台周期任务,与 `triggerFlush.staleMinutes`(API 热路径)不同 | §4.8 (Part 2) | +| `vm.metadata.triggerFlush.staleMinutes` | Long | 10 | `triggerFlushForVm()` 内联 stale claim 接管阈值(分钟):API 热路径中,若 dirty 行的 `lastClaimTime` 超过此时长且认领 MN 与传入的 `staleId` 一致,允许当前 MN 接管。与 `staleClaim.thresholdMinutes`(后台扫描 30 min)形成两级保护,详见 DP-06 | §3.1 (Part 2) | +| `vm.metadata.delete.maxRetry` | Integer | 3 | `deleteMetadata` 同步重试最大次数(ExpungeVmInstanceFlow 中使用) | §2 (Part 1c) | +| `vm.metadata.delete.baseDelaySec` | Long | 30 | `deleteMetadata` 同步重试基础延迟(秒),退避公式 `baseDelay × 2^retryIndex` | §2 (Part 1c) | +| `vm.metadata.lastRefreshVersion` | String | _(内部)_ | 升级全量刷新去重标记:记录最近一次已完成的升级刷新版本号,避免双 MN 重复触发。**仅供内部使用,运维不应手动修改** | §9 | + +--- + +# 14. 可观测性指标 + +以下指标建议通过 ZStack 内置 Prometheus 埋点暴露,供 Grafana 看板使用。 + +| 指标名 | 类型 | 标签 | 说明 | +|----------|------|------|------| +| `vm_metadata_flush_total` | Counter | status={success,fail,skip} | 刷写总次数,按结果分类 | +| `vm_metadata_flush_duration_seconds` | Histogram | — | 单次刷写耗时(从 buildMetadata 到 Agent 返回) | +| `vm_metadata_dirty_queue_size` | Gauge | — | 当前未认领的 dirty 行数(每轮 Poller 统计) | +| `vm_metadata_poller_cycle_duration_seconds` | Histogram | — | 单轮 Poller 执行总耗时(含认领 + 提交) | +| `vm_metadata_registration_total` | Counter | status={success,fail,rollback} | 注册总次数 | +| `vm_metadata_retry_count` | Histogram | — | 每次刷写成功时的累计重试次数分布 | + +**告警规则建议**: +- `vm_metadata_dirty_queue_size > 500 持续 5 分钟` → WARN(刷写积压) +- `rate(vm_metadata_flush_total{status="fail"}[5m]) > 10` → WARN(批量失败) +- `vm_metadata_flush_duration_seconds{quantile="0.99"} > 30` → WARN(单次刷写太慢) + +--- + +# 15. 约束与不変量 + +| 约束 ID | 内容 | 来源章节 | +|---------|------|----------| +| C-02B-1 | `nodeLeft()` 接管必须延迟 5s 后触发 `claimAndFlush()`,不得立即抢占 | §7.2 | +| C-02B-2 | 执行 sblk 写入前必须校验 dirty 行 `managementNodeUuid == 本 MN`,失去认领立即放弃写入 | §7.6 | +| C-02B-3 | 路径巡检禁止 `dbf.listAll` 全量加载,必须采用 keyset 分页(`vmInstanceUuid > lastUuid`) | §8.2.5 | +| C-02B-4 | 升级全量刷新必须按批(默认 1000)执行批量 SQL,避免单次超大事务 | §9.2 | +| C-02B-5 | payload 上限必须同时满足静态阈值(30MB)与运行时 slot 容量校验,容量不足先扩容再写入 | §10.0, §10.2 | +| C-02B-6 | `storageStructureChange` 标志保持 OR 语义,直到 dirty 行成功删除前不得降级为 false | §12.4 | +| C-02B-7 | 容量计算常量(Header/SlotHeader/MAX_LV)必须集中定义并用于公式推导,禁止硬编码散落 | §10.0 | +| C-02B-8 | `VmMetadataPathFingerprintVO.lastFlushFailed` 仅在重试耗尽时置 true,仅由 `MetadataStaleRecoveryTask` 重置为 false,其他路径不得修改 | §8.2.3 (M1 修复) | +| C-02B-9 | 升级全量刷新执行前必须检查最近 15 分钟内无 `nodeLeft` 事件,否则延迟重试 | §9.1 (M3 修复) | +| C-02B-10 | `nodeLeft` 延迟(`vm.metadata.nodeLeft.delaySec`)调整需与 Fence Check 机制配合评估,不得单独修改 | §7.2, §7.6 (M2 修复) | +| C-02B-11 | `false→true` 初始化必须使用分批 + 批间延迟,禁止一次性全量 INSERT dirty 行 | §9a.1 | +| C-02B-12 | `APICleanupVmInstanceMetadataMsg` 必须在 `vm.metadata.enabled=false` 时才允许执行,`true` 时拒绝 | §9a.2, Part 5 §6.3 | +| C-02B-13 | `false→true` 初始化任务每批必须重新检查 `vm.metadata.enabled` 开关状态,关闭时立即中止 | §9a.1 | +| C-02B-14 | 孤儿元数据检测仅报告不自动删除,避免与进行中的存储迁移竞态导致误删 | §8.4.3 | diff --git "a/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" "b/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" new file mode 100644 index 00000000000..3b7cfe09520 --- /dev/null +++ "b/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" @@ -0,0 +1,572 @@ +# VM 元数据 — 注册与运维 + +## 目录 + +1. [注册字段处理矩阵](#1-注册字段处理矩阵) +2. [跨存储数据盘处理规则](#2-跨存储数据盘处理规则) +3. [注册虚拟机详细流程](#3-注册虚拟机详细流程) +4. [注册事务回滚](#4-注册事务回滚) +5. [注册场景问题分析](#5-注册场景问题分析) +6. [可观测性](#6-可观测性) +7. [设计决策汇总](#7-设计决策汇总) +8. [运维指南:注册失败后的清理](#8-运维指南注册失败后的清理) +9. [约束与不変量](#9-约束与不変量) + +**API 定义**(请求/响应/错误码)统一见 [Part 5: API 设计](vm-metadata-05-API设计.md)。本文档不重复定义 API 结构。 + +## 0. 依赖声明 + +| 依赖项 | 类型 | 来源 | 本文使用方式 | +|--------|------|------|-------------| +| `VmMetadataPathFingerprintVO.vmInstanceUuid` | 数据模型约束 | [Part 2b §1](vm-metadata-02b-高可用与运维.md#1-高可用策略) | 字符串 UUID 作为稳定锚点,支持 keyset 分页与跨环境映射 | +| sblk 读取状态语义(OK/NEED_REPAIR/RECOVERED/DEGRADED/CORRUPTED) | 读取契约 | [Part 4d §2.4](vm-metadata-04d-sblk读取与恢复.md#24-readresult-状态语义) | 注册前读取元数据与注册后校验的可用性判定 | +| `APICheckVmInstanceMetadataConsistencyMsg` | 运维 API | [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性) | 注册完成后的一致性复核与告警触发 | + +--- + +## 1. 注册字段处理矩阵 + +### 1.1 VmInstanceVO + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| uuid | 保留 | 冲突时拒绝注册 | +| name | 保留 | — | +| description | 保留 | — | +| zoneUuid | API 参数 | 必填 | +| clusterUuid | API 参数 | 必填,赋值到 VO | +| hostUuid | 设 null | 注册后 VM 为 Stopped 状态 | +| lastHostUuid | 设 null | 新环境无意义 | +| instanceOfferingUuid | 设 null | 新环境可能不存在 | +| imageUuid | 保留原值,目标环境不存在时置 null | 若 `dbf.findByUuid(imageUuid, ImageVO.class) == null` 则 `imageUuid = null`,并在 warnings 中记录 `"imageUuid {xxx} not found in target environment, set to null"` | +| cpuNum | 保留 | — | +| memorySize | 保留 | — | +| platform | 保留 | — | +| architecture | 保留 | — | +| hypervisorType | 保留 | — | +| type | 保留 | 保持原值(UserVm) | +| state | 硬编码 | Registering → Stopped | +| defaultL3NetworkUuid | 设 null | 网络不恢复 | +| managementNetworkUuid | 设 null | 网络不恢复 | +| accountUuid | 替换 | 当前调用者(admin) | + +### 1.2 VolumeVO + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| uuid | 保留 | 冲突时拒绝注册 | +| primaryStorageUuid | 替换 | 新主存储 UUID | +| installPath | 替换 | 路径映射(vg uuid / 挂载路径替换) | +| diskOfferingUuid | 设 null | 新环境可能不存在 | +| vmInstanceUuid | 保留 | 与注册 VM UUID 一致 | +| accountUuid | 替换 | 当前调用者 | + +### 1.3 VolumeSnapshotVO + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| uuid | 保留 | 冲突时拒绝注册 | +| primaryStorageUuid | 替换 | 新主存储 UUID | +| primaryStorageInstallPath | 替换 | 路径映射 | +| volumeUuid | 保留 | — | +| parentUuid | 保留 | 快照链关系 | + +### 1.4 SystemTagVO / ResourceConfigVO + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| id | 自增 | 数据库自动生成 | +| uuid | 重新生成 | `Platform.getUuid()` | +| resourceUuid | 保留 | 指向 VM/Volume 的 UUID 不变 | +| 其余字段 | 保留 | — | + +**重要**:元数据中的 SystemTag/ResourceConfig 已在构建时经白名单过滤(见 [Part 1a §4.1](vm-metadata-01a-数据模型与序列化.md#41-systemtagresourceconfig-构建时过滤规则)),注册时**直接恢复到 DB,无需二次过滤**。 + +### 1.5 VolumeSnapshotGroupVO(快照组) + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| uuid | 保留 | 冲突时拒绝注册 | +| name | 保留 | — | +| description | 保留 | — | +| vmInstanceUuid | 保留 | 与注册 VM UUID 一致 | +| snapshotCount | 保留 | — | +| accountUuid | 替换 | 当前调用者(admin) | +| createDate | 保留 | — | +| lastOpDate | 重新生成 | 注册时间 | + +### 1.6 VolumeSnapshotGroupRefVO(快照组引用) + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| id | 自增 | 数据库自动生成 | +| volumeSnapshotGroupUuid | 保留 | FK → VolumeSnapshotGroupVO(同事务内已创建) | +| volumeSnapshotUuid | 保留 | FK → VolumeSnapshotVO(同事务内已创建) | +| volumeUuid | 保留 | FK → VolumeVO(同事务内已创建) | +| deviceId | 保留 | 磁盘设备编号 | +| volumeType | 保留 | Root / Data | +| volumeName | 保留 | — | +| volumeSnapshotName | 保留 | — | +| volumeSnapshotInstallPath | 替换 | 路径映射 | +| snapshotDeleted | 保留 | 反映原始删除状态 | +| volumeLastAttachDate | 保留 | 原始挂载时间 | +| createDate | 保留 | — | +| lastOpDate | 重新生成 | 注册时间 | + +### 1.7 VolumeSnapshotReferenceVO(快照引用记录) + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| id | 重新生成 | auto-increment(仅作为存储主键,不作为映射锚点) | +| volumeUuid | 保留原值 | 缓存 VM 的卷 UUID(无 FK,允许悬挂) | +| volumeSnapshotUuid | 保留原值 | 缓存 VM 的快照 UUID(无 FK,允许悬挂) | +| volumeSnapshotInstallUrl | 替换 | 路径映射 | +| directSnapshotUuid | 保留原值 | 无 FK,允许悬挂 | +| directSnapshotInstallUrl | 替换 | 路径映射 | +| treeUuid | 保留 | 指向 VolumeSnapshotReferenceTreeVO.uuid | +| parentId | 直接设 null | 注册场景等效于模板缓存已删除状态,FK `ON DELETE SET NULL` 已将 parentId 置 null,无需映射回填 | +| referenceUuid | 保留原值 | — | +| referenceType | 保留 | — | +| referenceInstallUrl | 替换 | 路径映射 | +| referenceVolumeUuid | 保留 | 子 VM 自己的卷 UUID(FK → VolumeEO CASCADE) | +| createDate | 保留 | — | +| lastOpDate | 重新生成 | 注册时间 | + +### 1.8 VolumeSnapshotReferenceTreeVO(快照引用树) + +| 字段 | 处理方式 | 说明 | +|------|----------|------| +| uuid | 保留 | 冲突时跳过(幂等,多个子 VM 可能共享同一棵树) | +| rootImageUuid | 保留原值 | 无 FK | +| rootVolumeUuid | 保留原值 | 无 FK,允许悬挂 | +| rootInstallUrl | 替换 | 路径映射 | +| rootVolumeSnapshotUuid | 保留原值 | 无 FK,允许悬挂 | +| rootVolumeSnapshotTreeUuid | 保留原值 | 无 FK,允许悬挂 | +| primaryStorageUuid | 替换 | 新主存储 UUID | +| hostUuid | 按需处理 | Local 存储保留,SharedBlock 设 null | + +--- + +## 2. 跨存储数据盘处理规则 + +### 2.1 策略 + +虚拟机的所有磁盘必须位于同一主存储,否则**拒绝注册**。 + +**原因**:跨存储路径映射规则不统一,快照组跨存储引用不完整,单存储简化所有流程。 + +**拒绝返回信息(改进)**:`CROSS_STORAGE_REJECTED: VM {vmUuid} has volumes on multiple primary storages: expectedPsUuid={targetPsUuid}, actualPsUuids={ps1,ps2,...}. Registration requires all volumes on one primary storage.` + +### 2.2 SnapshotGroup 处理 + +所有磁盘在同一存储上 → SnapshotGroup 天然完整。SnapshotGroupVO 和 SnapshotGroupRefVO 在同一事务内一次性创建。 + +--- + +## 3. 注册虚拟机详细流程 + +### 3.1 状态流 + +``` +(new) → Registering → Stopped → Starting → Running + │ + └── 失败 → 回滚删除所有 VO +``` + +### 3.2 "注册 VM 未首次启动" ResourceConfig + +| 时机 | 操作 | +|------|------| +| 注册完成 | 创建 `vm.metadata.registered.not.started` ResourceConfig | +| VM 首次到达 Running 状态 | 删除该 ResourceConfig,立即触发 `markDirty` | +| 存在该 ResourceConfig 时 | 任何 `@MetadataImpact` API 的元数据更新被跳过 | + +**注册 VM + 普通 API 交互**:注册完成后、首次启动前,VM 处于 Stopped 状态且持有 `registered.not.started` ResourceConfig。 +此时若执行 `APIUpdateVmInstanceMsg`(改名/描述),`VmMetadataUpdateInterceptor.afterCompletion()` 检测到 +ResourceConfig 存在,跳过 `markDirty`。改名/描述的变更不会即时反映到元数据中。 +**这是设计意图**:注册 VM 在未启动前具有完整的原始状态元数据(Step 7 markDirtyInternal 已写入)。 +用户在 Stopped 阶段做的修改(改名等)将在 VM 首次到达 Running 时通过删除 ResourceConfig + markDirty 一次性同步。 +若业务上无法接受此延迟,可通过 `APIUpdateVmMetadataMsg` 手动触发(该 API 绕过 ResourceConfig 检查)。 + +### 3.3 完整注册步骤 + +``` +1. 前置校验 + ├── 元数据 JSON 解析 + Base64 解码 + Validator 校验 + ├── readStatus 可用性检查(见下方说明) + ├── vmCategory 类型检查 + │ ├── REGULAR / TEMPLATE → 继续注册 + │ ├── TEMPLATE_CACHE → 拒绝注册 + │ └── null(旧版元数据) → 视为 REGULAR,继续 + ├── schemaVersion 精确匹配检查(见 Part 1a §6.2) + ├── 跨存储校验:所有 Volume 归属同一目标主存储(见 §2),失败返回 expected/actual PS UUID 列表 + ├── UUID 冲突检测(VM/Volume/Snapshot/SnapshotGroup/SnapshotGroupRef/Reference/ReferenceTree) + │ ├── **批量检测策略**:所有待检测 UUID 按每批 1000 个分组查询(`SELECT uuid FROM XxxVO WHERE uuid IN (:batch)`), + │ │ 避免单次 IN 子句超过数千个参数时的 SQL 解析性能退化。对于大快照链场景(54 磁盘 × 256 快照)UUID 总数可达万级。 + │ ├── 冲突且是 Registering 遗留 → 幂等回滚后重新注册 + │ └── 冲突且是正常资源 → 拒绝 + └── installPath 替换 + 路径存在性检查(Agent 校验) + ├── Root Volume installPath 不存在 → BLOCK(拒绝注册) + └── Data Volume installPath 不存在 → WARN(允许继续) + + readStatus 可用性校验逻辑: + + ```java + // 从 metadataContent JSON 中提取 __readStatus(Read API 嵌入) + String readStatus = metadata.get("__readStatus"); + if ("CORRUPTED".equals(readStatus) || "STORAGE_CHANGE_INCOMPLETE".equals(readStatus)) { + throw new ApiMessageInterceptionException(argerr( + "METADATA_READ_STATUS_UNUSABLE: metadata readStatus is %s, " + + "cannot register. Please resolve the storage issue and re-read metadata.", + readStatus)); + } + ``` + + **背景**:Register API 接收的 `metadataContent` 通常来自 Read API。Read API 在返回时将 `__readStatus` 字段嵌入 JSON 根级别。Register 入口解析此字段,对 `CORRUPTED`(双 Slot 损坏)和 `STORAGE_CHANGE_INCOMPLETE`(存储拓扑变更未完成)状态拒绝注册。`OK`/`NEED_REPAIR`/`RECOVERED`/`DEGRADED` 状态允许继续。若 `__readStatus` 字段不存在(手动构造的 JSON),视为 OK 继续。 + + PreCheck 判定示例: + + ```java + if (volume.isRootVolume() && !pathExists(volume.getInstallPath())) { + result.add(PreCheckItem.block(INSTALL_PATH_EXIST, + "Root volume install path does not exist: " + volume.getInstallPath())); + } else if (!pathExists(volume.getInstallPath())) { + result.add(PreCheckItem.warn(INSTALL_PATH_EXIST, + "Data volume install path does not exist: " + volume.getInstallPath())); + } + ``` + +2. 创建 VmInstanceVO + ├── state = Registering + ├── 打 SystemTag: vmMetadata::registeringMnUuid::{mnUuid} + ├── 打 SystemTag: vmMetadata::registeringStartTime::{timestamp} + └── 创建 "注册VM未首次启动" ResourceConfig + +3. 还原 SystemTag / ResourceConfig + ├── 从元数据中解码(Base64 解码) + ├── 直接恢复到 DB(构建时已过滤,无需二次过滤) + └── 为每个 SystemTag/ResourceConfig 生成新 UUID(Platform.getUuid()) + +4. 创建 VolumeVO + ├── 替换 primaryStorageUuid、installPath、accountUuid + └── 还原 volume 级 SystemTag / ResourceConfig + +5. 快照还原 + ├── 每棵快照树使用 VolumeSnapshotTree.fromInventories() 构建 + │ ├── 创建 VolumeSnapshotTreeVO + │ ├── 层级遍历快照树,按顺序创建 VolumeSnapshotVO + │ └── 校验每个 parentUuid 在已创建集合中存在 + ├── 创建 VolumeSnapshotGroupVO + VolumeSnapshotGroupRefVO + ├── 创建 VolumeSnapshotReferenceVO + VolumeSnapshotReferenceTreeVO + └── 事务策略:批量 persist 每 100 条 flush + clear + + **大快照链性能说明**:极端场景下(24 磁盘 × 256 快照 = 6144 个 VolumeSnapshotVO + Group/Ref/Tree 关联记录),单事务写入量可达万级别。当前使用 `batch flush+clear per 100 rows` 缓解 JPA 一级缓存膨胀。若快照总数超过 1000,在 LongJob 进度中记录预计耗时,并在 `warnings` 中提示 `"Large snapshot chain detected ({count} snapshots), registration may take longer than usual"`。v2+ 考虑分卷分事务策略降低 Galera 复制延迟风险。 + +6. 执行变基(sblk / local / NFS) + ├── **变基前重新校验 installPath 存在性**(Agent 调用) + │ └── 若 Root Volume installPath 不存在 → 直接进入回滚路径,不执行 qemu-img 操作 + │ (Step 1 的校验与 Step 6 之间可能经过数分钟 VO 创建,存储侧可能发生变化) + ├── 幂等:先 qemu-img info 检查当前 backing file + │ ├── 已指向目标路径 → 跳过 + │ ├── 指向旧路径 → 执行 qemu-img rebase -u + │ └── 指向异常路径 → 报错 + └── 变基失败 → 整个注册回滚 + +7. 注册成功 + ├── 更新 VmInstanceVO.state = Stopped + 删除 registeringMnUuid tag(同一事务内,保证原子性) + ├── 事务提交后立即调用 `markDirtyInternal(vmUuid, true)` ← 绕过 ResourceConfig 检查,确保首次元数据写入 + │ **markDirtyInternal 机制说明**:`markDirtyInternal` 并非一个独立方法,而是直接调用 `markDirty()` 的内部路径。 + │ 拑制发生在 `VmMetadataUpdateInterceptor.afterCompletion()` 中——当检测到 + │ `registered.not.started` ResourceConfig 存在时,拦截器 skip `markDirty` 调用。 + │ 注册 Step 7 的 `markDirtyInternal` 不经过拦截器,而是从服务内部直接调用 + │ `VmMetadataDirtyMarker.markDirty(vmUuid, storageStructureChange=true)`,因此不受 ResourceConfig 拑制。 + ├── 异步触发 `APICheckVmInstanceMetadataConsistencyMsg(autoRepair=true)` 做注册后一致性复核;若发现不一致,在 Event.warnings 中记录差异项 + └── 返回结果(含 warnings) + + **注意**:`registered.not.started` ResourceConfig **不在此步删除**。该 Config 的完整生命周期见 §3.2:创建于 Step 2 → 抑制 Stopped 阶段的 `@MetadataImpact` API 触发 markDirty → VM 首次到达 Running 时删除并触发 markDirty。 +``` + +### 3.4 sblk 变基详细流程 + +``` +原存储: sblkA (vg_uuid = "123xxx") +新存储: sblkB (vg_uuid = "456xxx") + +步骤: + 1. 替换 VO 中 vg uuid: 123xxx → 456xxx(前缀锚定替换) + 2. 校验替换后 installPath 在存储上存在 + 3. 创建所有 VO + 4. 变基: qemu-img rebase -u -b <新backing路径> <当前LV路径> +``` + +**路径替换安全机制**:使用 `String.startsWith(oldPrefix)` 检查后字符串拼接,路径格式通过正则预校验。 + +**分隔符边界保护**:前缀锚定时要求 `oldPrefix` 以路径分隔符结束(如 `/oldVg/`),并验证替换点满足边界条件,避免将 `oldVg` 误命中为 `oldVg2` 或其他子串。 + +### 3.5 Registering 状态 VM 的可见性 + +- `Registering` 状态 VM **仅 admin 可见** +- 普通用户 `QueryVmInstance` 自动过滤 +- admin 可查询但变更操作被拦截器拒绝 + +**Registering 状态 API 拦截实现位置**:在 `VmInstanceBase` 中统一处理,而非在每个 API handler 中单独检查。`VmInstanceBase.handleMessage()` 入口处增加 `state == Registering` 检查:除 `QueryVmInstanceMsg` 和内部注册消息外,所有其他消息均返回 `VM_IN_REGISTERING_STATE` 错误码。这避免了在新增 API 时遗漏添加 Registering 状态拦截的风险。 + +--- + +## 4. 注册事务回滚 + +### 4.1 回滚触发条件 + +MN 启动时扫描 `state=Registering` 的 VM,检查 `registeringMnUuid` SystemTag: + +| tag 中 mnUuid | 条件 | 行为 | +|---------------|------|------| +| = 当前 MN UUID | — | 回滚 | +| ≠ 当前 MN UUID | 该 MN **不在线** | 回滚 | +| ≠ 当前 MN UUID | 该 MN **在线** | 跳过 | + +**触发时机**:`managementNodeReady()` / `ManagementNodeLeftEvent` 回调。 + +### 4.2 回滚操作 + +注册流程中通过 `Set createdVoUuids` 跟踪每步实际创建的 VO UUID。回滚时仅删除此集合中的记录,而非尝试删除“应该存在”的所有元数据对象——避免崩溃发生在中间步骤时删除未创建的 VO 引发的无效查询或 FK 异常。 + +按以下顺序删除当前注册创建的所有 VO: + +1. VolumeSnapshotReferenceTreeVO(外层对象;**删除前检查依赖**:若该 TreeVO 下仍有其他 VM 的 ReferenceVO 行(多个子 VM 共享同一棵树),则保留 TreeVO 不删除,仅删除当前 VM 的 ReferenceVO 行) +2. VolumeSnapshotGroupRefVO / VolumeSnapshotGroupVO +3. VolumeSnapshotVO +4. VolumeVO(含 SystemTag / ResourceConfig) +5. VmInstanceVO(含 SystemTag / ResourceConfig) + +回滚顺序采用“由外到内”原则,优先删除聚合根对象,利用数据库级联减少中途失败导致的残留。 + +**回滚后防御性清理 SQL**(幂等,可重复执行): + +```sql +DELETE t FROM VolumeSnapshotReferenceTreeVO t +LEFT JOIN VolumeSnapshotReferenceVO r ON r.treeUuid = t.uuid +WHERE r.id IS NULL; +``` + +**存储数据不删除**:存储上的数据是用户迁移的,不因注册失败而删除。 + +### 4.3 幂等与可重入 + +每步 `DELETE` 天然幂等。回滚中途再次崩溃 → 下次启动重新检测到 `Registering` → 继续回滚。 + +### 4.4 LongJob 超时与取消回滚 + +- 注册 LongJob 超时时,`cancel()` 必须调用 `rollbackRegistration(vmUuid)`,复用与失败路径相同的回滚逻辑。 +- 超时后的中间态 VM 必须保留在 `Registering`,使后续 UUID 冲突检测可识别为“Registering 遗留”并触发幂等回滚重试。 +- **LongJob 超时时长来源**:从 `APIRegisterVmInstanceFromMetadataMsg` 的 API timeout 配置推导(默认 30 分钟),而非硬编码。ChainTask 超时 = API timeout + 5 分钟余量。`registeringStartTime` 过期判定同样基于此配置值 + 5 分钟,而非硬编码 35 分钟。这允许运维通过调整 API timeout 统一控制注册超时行为。 + +--- + +## 5. 注册场景问题分析 + +### 5.1 UUID 冲突 + +注册前批量查询所有涉及的 UUID,任一冲突立即拒绝。检测到冲突时判断是否为 Registering 遗留 → 是 → 回滚后重新注册。 + +### 5.2 installPath 映射 + +路径映射采用**自动推导**,无需用户手动提供: + +| 存储类型 | 旧路径标识符来源 | 新路径标识符来源 | +|----------|----------------|----------------| +| sblk | 从元数据 VolumeVO.installPath 提取旧 VG UUID | 目标主存储 VG UUID | +| local/NFS | 从元数据 VolumeVO.installPath 提取旧挂载路径 | 目标主存储 mountPath | + +**文件不移动**:注册流程中不移动文件。账户替换只在 DB 层面。 + +### 5.3 元数据损坏/不完整 + +JSON 解析 / Base64 解码 / 校验器任一步骤失败 → 拒绝注册。sblk 双 Slot 容错机制详见 [Part 4d](vm-metadata-04d-sblk读取与恢复.md)。 + +### 5.4 快照链变基的幂等性 + +| 当前 backing file | 行为 | +|-------------------|------| +| 已指向目标路径 | 跳过 | +| 指向旧路径 | 执行变基 | +| 指向其他路径 | 报错 | + +### 5.5 部分快照树失败 + +原子性以 **VM 为粒度**:任一快照树失败 → 整个注册回滚。 + +### 5.6 并发操作 + +- Registering 状态 VM 只允许查询 +- ChainTask `syncSignature = vm-register-{vmUuid}` +- ChainTask 超时:`timeout = API timeout + 5 分钟余量`(见 §4.4,从 API 配置推导) +- **DB 主键是跨 MN 互斥的最终保证**:`INSERT VmInstanceVO(uuid=xxx, state=Registering)` 的主键重复即拦截并发注册。即使两个 MN 同时对同一 VM 发起注册,先提交的事务成功,后提交的因主键冲突失败。 + +**注册部分创建窗口分析**:Step 2 创建 VmInstanceVO 与 Step 5 创建快照之间可能经过数分钟(大快照链)。在此窗口内 VM 处于 Registering 状态,只允许查询操作(见此节第一条),且 Registering 状态的 VM 不会被 Poller 处理(无 dirty 行,因 markDirty 发生在 Step 7)。因此部分创建状态对外部操作不可见、不会被操作,安全。 + +### 5.7 无网卡 VM 的启动行为 + +注册后 VM 无网卡是允许的状态。推荐流程:先加网卡(`AttachVmNicToVm`),再启动。 + +#### 5.7.1 为什么不恢复网络 + +注册不恢复网络(NIC、L3 绑定、IP 分配、安全组),原因如下: + +1. **L3 网络拓扑不可迁移**:源环境的 L3 网络 UUID、VLAN ID、CIDR 在目标环境中不存在或不相同 +2. **IP 地址冲突风险**:源环境的 IP 可能已在目标环境中被分配给其他 VM +3. **安全组/VPC 规则依赖环境**:防火墙规则引用的 SecurityGroup UUID、VPC UUID 均为环境专属 + +#### 5.7.2 手动网络恢复步骤 + +``` +1. 查看注册 warnings 中输出的原始 NIC 信息: + "Original NIC config: {nicUuid, l3NetworkUuid, ip, mac, deviceId}" + +2. 在目标环境中选择对应的 L3 网络: + - 若已有匹配网段的 L3 → 直接使用 + - 若无 → 先创建 L3Network + IP Range + +3. 挂载网卡: + APIAttachVmNicToVmMsg(vmInstanceUuid, l3NetworkUuid) + → 系统自动分配 IP + MAC + +4. (可选)恢复安全组绑定: + APIAddVmNicToSecurityGroupMsg(securityGroupUuid, vmNicUuids) + +5. 启动 VM: + APIStartVmInstanceMsg(uuid) +``` + +**注意**:注册时会在 `warnings` 中输出所有原始 NIC 配置信息(`l3NetworkUuid`、`ip`、`mac`、`deviceId`),供运维参考。目标环境中 MAC 地址会重新生成,不会与源环境冲突。 + +### 5.8 链式克隆虚拟机注册 + +#### 核心原则 + +注册后的子 VM 等效于**模板和缓存已被删除**的状态。不恢复模板、缓存及其快照的 DB 记录。 + +#### 注册流程差异 + +| 项目 | 普通 VM | 链式克隆子 VM | +|------|---------|--------------| +| VmInstanceVO | 创建 | 创建 | +| VolumeVO | 创建 | 创建 | +| VolumeSnapshotVO | 恢复子 VM 自己的 | 恢复子 VM 自己的 | +| VolumeSnapshotReferenceTreeVO | 不涉及 | check existence → skip or create | +| VolumeSnapshotReferenceVO | 不涉及 | 直接插入,parentId 统一置 null | + +#### parentId 处理策略 + +注册场景等效于**模板缓存已被删除**的状态。缓存 VM 删除时,`VolumeSnapshotReferenceVO.parentId` 的 FK(`ON DELETE SET NULL`,自引用)已将所有子引用的 parentId 置为 null。因此注册时无需映射回填,直接全量插入 `parentId = null` 即可。 + +``` +1. 插入全部 VolumeSnapshotReferenceVO,parentId 统一置 null +2. 无需第二阶段回填 +``` + +**简化理由**:注册的子 VM 不可能依赖缓存 VM 的其他 Reference 行(缓存 VM 未被注册),因此 parentId 引用链在新环境中天然为空。 + +#### TreeVO 幂等性 + +多个子 VM 可能共享同一棵树。使用 UUID 做存在性检查: + +```java +if (!Q.New(VolumeSnapshotReferenceTreeVO.class) + .eq(VolumeSnapshotReferenceTreeVO_.uuid, treeVO.getUuid()) + .isExists()) { + dbf.persist(treeVO); +} +``` + +--- + +## 6. 可观测性 + +### 6.1 运维告警 + +新增报警器:**更新虚拟机元数据失败**。触发条件:达到最大重试次数仍失败。 + +### 6.2 一致性检查 + +`APICheckVmInstanceMetadataConsistencyMsg`:从 DB 构建元数据 → 从存储读取 → 结构化比较。排除 `lastOpDate`、`id`、`managementNodeUuid` 字段。 + +API 详细定义见 [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性)。 + +### 6.3 注册预检查 + +`APIPreCheckVmMetadataRegistrationMsg`:检查 UUID 冲突、PS 可达性、版本兼容等。 + +API 详细定义见 [Part 5 §6.2](vm-metadata-05-API设计.md#62-注册预检查)。 + +### 6.4 手动触发元数据更新 + +`APIUpdateVmMetadataMsg`:指定 vmUuid,手动触发一次全量元数据更新。 + +API 详细定义见 [Part 5 §6.1](vm-metadata-05-API设计.md#61-手动触发元数据更新)。 + +--- + +## 7. 设计决策汇总 + +| 问题域 | 决策 | 理由 | +|--------|------|------| +| UUID 冲突 | 前置全量检查 + Registering 幂等回滚 | 防重复注册 | +| MN 崩溃 | SystemTag 标记 + 启动扫描 | 防中间状态泄漏 | +| 版本不匹配 | 默认拒绝 + `forceVersionMismatch` 允许强制注册 | 兼顾安全性和灵活性。**`forceVersionMismatch=true` 时字段映射策略**:(1) 目标版本新增但源版本缺失的字段 → 使用 Java 默认值(null/0/false),Gson 反序列化自动处理;(2) 源版本存在但目标版本已移除的字段 → 忽略(Gson 默认丢弃未知字段);(3) 字段类型变更 → Gson 抛异常,注册失败并在 warnings 中列出被忽略/使用默认值的字段名列表 | +| 路径映射 | 自动推导 + 前缀锚定替换 + 文件存在性检查 | 简单可靠 | +| 跨存储 | 拒绝注册 | 消除复杂性 | +| 跨存储错误信息 | 返回 expected/actual PS UUID 明细 | 运维可直接定位冲突卷 | +| SystemTag 过滤 | 构建时白名单过滤,注册时直接恢复 | 无需二次过滤 | +| 模板 VM | 注册为普通 VM | 纯标记表无业务字段。**注意**:注册后不保留模板身份,若需要恢复模板功能需手动转换(`APIChangeVmInstanceToTemplateMsg`)。注册时 warnings 中记录 `"VM {uuid} was a template VM, registered as regular VM"`。**降级后丢失的能力**:(1) 不能从此 VM 创建链式克隆子 VM;(2) 不能作为模板发布到镜像市场;(3) 不能被其他用户用作创建 VM 的模板。**恢复方式**:调用 `APIChangeVmInstanceToTemplateMsg(vmInstanceUuid)` 将 VM 转回模板 | +| 缓存 VM | 写入元数据但拒绝注册 | 运行态产物,新环境自动创建 | +| 链式克隆子 VM | 仅恢复 Reference 表 | 等效于模板已删除状态 | +| Reference parentId 映射 | 直接置 null(等效模板缓存已删除) | 无需映射回填,简化注册流程 | +| 存储数据 | 注册回滚不删除 | 数据由用户迁移 | +| 变基幂等 | `qemu-img info` 预检查 | 支持安全重试 | +| 文件移动 | 不移动,仅 DB 替换 accountUuid | 避免大文件移动风险 | +| 注册超时 | API timeout + 5 分钟余量 ChainTask + cancel 回滚 | 从 API 配置推导,避免硬编码;防止 LongJob 超时残留 | +| Root 路径不存在 | `INSTALL_PATH_EXIST` 视为 BLOCK | Root 缺失不可启动 | +| 注册后校验 | 触发 ConsistencyCheck | 及早暴露存储/DB 偏差 | + +--- + +## 8. 运维指南:注册失败后的清理 + +### 8.1 自动清理 + +MN 启动时自动扫描 `state=Registering` 的 VM 并回滚(见 §4.1)。正常情况下无需手动干预。 + +### 8.2 手动清理场景 + +当自动回滚未成功时(极端场景),运维可按以下顺序手动清理: + +```sql +-- 1. 查找残留的 Registering VM +SELECT uuid, name FROM VmInstanceVO WHERE state = 'Registering'; + +-- 2. 按依赖顺序删除(先子后父) +-- 步骤同 §4.2 回滚操作:ReferenceTree→Reference→GroupRef→Group→Snapshot→Volume→VM +DELETE FROM VolumeSnapshotReferenceVO WHERE referenceVolumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); +DELETE FROM VolumeSnapshotGroupRefVO WHERE volumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); +DELETE FROM VolumeSnapshotVO WHERE volumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); +DELETE FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'; +DELETE FROM VmInstanceVO WHERE uuid = '{vmUuid}'; +``` + +**重要**:存储上的数据不删除。存储数据由用户迁移而来,不因注册失败而清理。 + +--- + +## 9. 约束与不変量 + +| 约束 ID | 约束描述 | 违反后果 | 检查点 | +|---------|----------|----------|--------| +| C-03-1 | `VolumeSnapshotReferenceVO.parentId` 注册时统一置 null(等效模板缓存已删除状态),不做映射回填 | — | 注册步骤 §3.3-5 与链式克隆 §5.8 | +| C-03-2 | 跨存储(同 VM 卷分布多个 PS)必须拒绝注册,并返回 expected/actual PS UUID 明细 | 错误路径映射、快照组不完整 | 前置校验 §2.1 / §3.3-1 | +| C-03-3 | installPath 前缀替换必须满足分隔符边界(`/oldPrefix/`) | 子串误替换导致路径污染 | 路径映射 §3.4 / §5.2 | +| C-03-4 | 回滚删除顺序必须“由外到内”,并执行空树清理 SQL | Tree/Reference 残留与数据泄漏 | 回滚 §4.2 | +| C-03-5 | 注册 ChainTask 超时从 API timeout 配置推导(+ 5 分钟余量);LongJob cancel 必须触发 `rollbackRegistration` | Registering 残留、后续冲突误判 | 并发与超时 §4.4 / §5.6 | +| C-03-6 | Root Volume `INSTALL_PATH_EXIST` 缺失必须 BLOCK;Data Volume 可 WARN | 注册成功但 VM 无法启动 | 前置校验 §3.3-1 | +| C-03-7 | 注册成功后必须触发一次 ConsistencyCheck | 存储与 DB 偏差延迟暴露 | 成功收敛 §3.3-7 / 可观测性 §6.2 | +| C-03-8 | PreCheck 与 Register **必须共享同一校验方法**(如 `validateRegistration()`),PreCheck = validate only,Register = validate + execute。新增校验项时只需修改一处 | PreCheck 通过但 Register 失败(或反之),用户体验矛盾 | 前置校验 §3.3-1 / PreCheck §6.3 / [Part 5 §6.2](vm-metadata-05-API设计.md#62-注册预检查) | diff --git "a/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" "b/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" new file mode 100644 index 00000000000..1d49ee78275 --- /dev/null +++ "b/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" @@ -0,0 +1,248 @@ +# VM 元数据 — sblk 二进制存储协议概述 + +## 1. 术语表 + +| 术语 | 定义 | +|------|------| +| sblk | ZStack 共享块存储(Shared Block Storage),基于 LVM 的裸块设备 | +| LV | LVM Logical Volume,元数据持久化的最小存储单元 | +| Header Block | LV 头部 4KB 区域,存放控制信息与 VM 摘要 | +| Slot | 数据槽,存放完整的 payload(元数据 DTO JSON) | +| Active Slot | Header.ActiveSlot 指向的当前有效 Slot | +| Inactive Slot | 未被 ActiveSlot 指向的另一个 Slot,写入目标 | +| PendingOp | Header 中的操作意图标记,0=空闲 / 1=CONFIG_UPDATE / 2=STORAGE_CHANGE | +| WriteSequence | 单调递增写计数器,用于判读 Slot 数据是否属于最新一次写入 | +| ControlChecksum | SHA-256(Header[0:64]),覆盖崩溃恢复关键字段 | +| SummaryChecksum | SHA-256(Header[96:896]),覆盖 VM 摘要字段 | +| O_DIRECT | Linux 直接 I/O 标志,绕过 page cache | +| ALIGNMENT | 4096 字节,O_DIRECT I/O 对齐粒度 | +| op_type | 写入操作类型,由控制面 `@MetadataImpact` 注解决定 | +| Full-refresh | 从管理面数据库重建完整元数据并全量写入 LV | + +--- + +## 2. 适用范围 + +本文档仅覆盖 **sblk(共享块存储)** 场景下的二进制存储协议。 + +| 场景 | 存储协议 | 并发控制 | 崩溃安全机制 | +|------|---------|---------|-------------| +| **sblk** | 本文档:二进制 Header + A/B Dual Slot | 管理面四层串行化 (Part 2 §3.1) | A/B Dual Slot + PendingOp | +| local/NFS | JSON 明文 + tmp + fsync + rename | flock (defense-in-depth) | 原子 rename | + +> local/NFS 场景不使用 op_type、PendingOp 等概念(JSON atomic rename 无中间状态)。后文所有设计均仅针对 sblk。 + +--- + +## 3. 背景与动机 + +ZStack 共享块存储(sblk)场景下,VM 元数据需要持久化到 LVM Logical Volume 上。多个管理节点可能通过共享块设备并发访问同一 LV。 + +核心挑战: + +- **无文件系统**:LV 是裸块设备,无法使用常规文件 I/O +- **共享访问**:多节点通过 O_DIRECT 绕过 page cache 直接读写 +- **崩溃安全**:任意时刻断电或进程崩溃后,数据必须可恢复 +- **空间受限**:LV 初始 4MB,最大 64MB,需高效利用 + +### 3.1 灾备接管 — A/B 双 Slot 的核心驱动力 + +> **脑裂不在设计范围内**:如果两个平台同时对同一 LV 执行写入操作,A/B Slot 不提供保护。预留 Header Reserved 区 8B 用于存储 `platformId`,未来可用于检测跨平台写入冲突。 + +除常规读写外,协议必须支持**跨平台灾备接管**场景: + +``` +环境: + sanA / sanB — 两套拥有相同 LUN(数量和大小)的 SAN 存储 + zsvA(原平台)/ zsvB(目标平台)— 两套独立的 ZSV 管理平台 + +操作流程: + 1. zsvA 将 sanA 添加为 sblk 存储,在上面创建 VM 并正常读写 + 2. zsvB 将 sanB 添加为存储目标(iSCSI server),但不注册为 sblk 存储 + 3. 存储侧配置 sanA → sanB 的 LUN 级数据复制(块级,平台不感知) + 4. zsvA 的 sanA 发生故障 + 5. zsvB 使用 sanB 注册 sblk 存储,通过扫描 LV 上的元数据恢复 VM +``` + +此场景下 LV 元数据的角色发生本质转变: + +| | 正常运行 | 灾备接管 | +|---|---------|---------| +| 元数据权威来源 | 管理面 DB | **LV 上的元数据** | +| LV 元数据角色 | DB 的副本/缓存 | **唯一的 VM 恢复来源** | +| 管理面 DB 可用? | (Y) zsvA DB 可用 | (N) zsvA 故障,zsvB DB 无此 VM | +| Full-refresh 可行? | (Y) 从 DB 重建 | (N) **无 DB 数据可重建** | + +**核心问题**:存储侧复制是**块级别快照**,可能捕获到 LV 正在写入的中间状态。 + +单区覆盖写在此场景下的风险: + +``` +zsvA 正在写入: 已写入部分新数据,旧数据已被覆盖 +此刻 sanA → sanB 块级复制发生 +sanB 上的 LV: payload 损坏 + 旧数据不可恢复 + zsvB 无 DB → VM 不可恢复 (N) +``` + +A/B 双 Slot 的保证: + +``` +zsvA 正在写入: Phase 2 写入 Inactive Slot,Active Slot 未被触碰 +此刻 sanA → sanB 块级复制发生 +sanB 上的 LV: Active Slot 完整有效 → zsvB 读到旧元数据 → VM 可注册 (Y) +``` + +> **结论**:A/B Dual Slot 是能保证任意复制时刻都有可读数据的最简方案。协议复杂度是为灾备可靠性买单。 + +--- + +## 4. 设计目标 + +| 目标 | 要求 | +|------|------| +| 原子性 | 任意崩溃点数据不损坏,已提交数据不丢失 | +| 自描述 | Slot 自带位置信息,Header 损坏时仍可恢复 | +| 高效 I/O | O_DIRECT + O_SYNC,对齐到 4KB 页边界 | +| 简单可靠 | 纯二进制定长字段,无 JSON 解析开销 | +| 可观测 | hexdump 直接可读,状态可诊断 | +| 前向兼容 | HeaderVersion 管布局演进,SchemaVersion 管 payload 演进 | + +--- + +## 5. 整体架构 + +LV 初始预分配 4MB 空间(虚拟机在正常使用场景下,元数据一般只有几十 KB)。直接以 Raw Data 存储 JSON 元数据,不格式化文件系统。采用 **预分配固定大小 LV + Raw Data 存储 + A/B 分区原子写** 方案,规避频繁创建/删除 LV 的性能问题。 + +``` +LV Layout (e.g. 4MB) +┌──────────────┬────────────────────┬────────────────────┐ +│ Header Block │ Slot A │ Slot B │ +│ 4KB │ ~2MB │ ~2MB │ +│ (4096B) │ │ │ +└──────────────┴────────────────────┴────────────────────┘ +offset: 0 4096 4096 + SlotACapacity + +空间计算公式(4KB 对齐): +available = LV_SIZE - 4096 +slot_capacity = floor(available / 2 / 4096) * 4096 + +示例(4MB LV): +available = 4194304 - 4096 = 4190208 +slot_capacity = floor(4190208 / 2 / 4096) * 4096 = 2093056 + +Header: [0, 4096) +Slot A: [4096, 2097152) 约 2043 KB +Slot B: [2097152, 4190208) 约 2043 KB +Tail: [4190208, 4194304) 约 4 KB (未使用) +``` + +- **Header Block (4096B)**:控制信息 + VM 摘要信息,O_DIRECT 单次写入 +- **Slot A / Slot B**:双槽交替写入,A/B 切换实现原子更新 + +### 5.1 已知 LV 大小集合(KNOWN_LV_SIZES) + +为了支撑扩容后恢复与多布局回退,协议约束可识别的历史 LV 大小集合为: + +```python +KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] +``` + +对应字节值(9 个固定值): + +```python +KNOWN_LV_SIZES = [ + 4 * 1024 * 1024, + 6 * 1024 * 1024, + 8 * 1024 * 1024, + 12 * 1024 * 1024, + 16 * 1024 * 1024, + 24 * 1024 * 1024, + 32 * 1024 * 1024, + 48 * 1024 * 1024, + 64 * 1024 * 1024, +] +``` + +该集合与 Part 4e 的阶梯扩容规则保持一致,供 Part 4d 的 multi-layout 恢复穷举使用。 + +### 5.2 A/B Dual Slot 工作模式 + +``` +正常状态 (ActiveSlot=A): + 读取 → Slot A (当前有效数据) + +写入时: + Phase 1 → 标记 intent 到 Header + Phase 2 → 写新数据到 Slot B (inactive) + Phase 3 → 切换 ActiveSlot 到 B + 清除 intent + +下次写入: + Phase 1 → 标记 intent + Phase 2 → 写新数据到 Slot A (此时 inactive) + Phase 3 → 切换 ActiveSlot 到 A +``` + +交替写入确保:**任意崩溃点至少有一个 Slot 包含完整有效数据**。 + +> **PendingOp 恢复**:若写入在 Phase 1~Phase 2 之间崩溃(Header.PendingOp≠0),读取时触发 `repair_pending_op` 恢复流程,通过双布局尝试(old-layout → new-layout)定位 Target Slot 并完成 Phase 3。完整恢复逻辑详见 [Part 4d §4](vm-metadata-04d-sblk读取与恢复.md)。 + +### 5.3 版本管理 + +两个独立的版本号,职责分离: + +| 版本号 | 位置 | 含义 | 何时递增 | +|--------|------|------|---------| +| HeaderVersion | Header Block | 二进制布局版本(字段偏移、大小、Checksum 算法) | 增删 Header/Slot 字段时 | +| SchemaVersion | Header Block | Payload JSON 业务 schema 版本 | Payload 中 JSON 字段增减时 | + +读取策略: +- `HeaderVersion > MAX_KNOWN` → 拒绝解析,提示升级软件 +- `SchemaVersion > MAX_KNOWN` → 可读出 payload,但提示部分字段可能无法识别 + +### 5.4 崩溃安全模型 + +> 本节为全文档共享的崩溃安全设计原则,Part 4c/4d/4e 中引用而不重复展开。 + +**核心声明**: +- **协议不依赖单次 4KB I/O 的原子性**。尽管当前主流 SSD/HDD 在扩区层面提供 512B~4KB 原子写入,但协议不将其作为安全假设。崩溃安全完全依赖 A/B Dual Slot 机制。 +- **读取路径不依赖 LV 大小计算 Slot 位置**。Slot 定位信息从 Header 中显式读取(`SlotAOffset`/`SlotBOffset`),而非从 `lv_size` 计算。这保证了 LV 扩容后,旧 Header 中的偏移仍然有效。 + +**部分写入分析**(Header 4KB 写入中途崩溃): + +| 崩溃时刻 | 已写入字段 | 未写入字段 | 影响 | +|----------|----------|----------|------| +| Phase 1 写 Header 中途 | PendingOp 可能已写 | ActiveSlot 未变 | Active Slot 完整,数据安全 | +| Phase 3 写 Header 中途 | ActiveSlot 可能已切换 | ControlChecksum 未更新 | 校验失败 → 进入恢复流程 → 从 Slot 自描述恢复 | +| Phase 3 完全成功 | 所有字段已写 | — | 正常 | + +> 即使单次 4KB 写入不原子,最坏情况是 Header 损坏 + ControlChecksum 不匹配,此时恢复流程(Part 4d §3)通过 Slot 自描述信息 + Checksum 找到有效数据。 + +**崩溃安全机制摘要**: + +1. Header 为 4KB,通过单次 O_DIRECT + O_SYNC 写入 +2. Phase 1 不切换 ActiveSlot → 崩溃后 Active Slot 定位信息完好 +3. Phase 2 写入 Inactive Slot → Active Slot 数据不受影响 +4. Phase 3 才切换 ActiveSlot + 更新布局 → 提交语义 +5. 即使 Header 写入中途崩溃(部分字段更新),恢复流程通过 Slot 自描述 + Checksum 找到有效数据 + +**VM 摘要区降级**:摘要区 [96, 928) 仅用于扫描优化,写入中途崩溃 → SummaryChecksum 校验失败 → 降级读 Slot,不影响正确性。 + +### 5.5 Python 2 兼容性 + +当前 Agent 环境为 Python 2.7(与 ZStack KVM Agent 一致),代码按 Python 2 编写: + +- `struct.pack/unpack` 处理大端序二进制 +- `ctypes` 分配对齐内存缓冲区(O_DIRECT 要求) +- `buffer()` 实现零拷贝写入 +- `hashlib.sha256` (Python 2.7+ 内置) +- Python 3 迁移随 Agent 整体迁移计划进行,不单独迁移 + +--- + +## 6. 文档导航 + +| 子文档 | 内容 | 典型读者 | +|--------|------|---------| +| [Part 4b — 二进制布局](vm-metadata-04b-sblk二进制布局.md) | Header Block 与 Slot 的字段定义、设计理由、hexdump 示例 | 协议实现者 | +| [Part 4c — 写入流程](vm-metadata-04c-sblk写入流程.md) | 三阶段原子写入、崩溃场景分析、状态转换图 | 协议实现者、CR 审查者 | +| [Part 4d — 读取与恢复](vm-metadata-04d-sblk读取与恢复.md) | 读取分支、Header 损坏恢复、`DEGRADED` 降级读取、multi-layout 修复与 Repair/Full-Refresh | 协议实现者、运维 | +| [Part 4e — 运维与 I/O 细节](vm-metadata-04e-sblk运维与IO.md) | LV 管理、扩容、初始化、健康检查、AlignedBuffer 代码 | Agent 开发者、运维 | diff --git "a/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" "b/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" new file mode 100644 index 00000000000..4d44207a168 --- /dev/null +++ "b/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" @@ -0,0 +1,289 @@ +# VM 元数据 — sblk 二进制布局 + +## 1. Header Block (4096 Bytes) + +Header 大小等于 O_DIRECT ALIGNMENT(4KB),单次对齐 I/O 即可完成读写。内部分为三个区域: + +| 区域 | 偏移范围 | 用途 | +|------|---------|------| +| 控制区 | [0, 96) | 崩溃恢复关键字段 + ControlChecksum | +| VM 摘要区 | [96, 928) | 扫描优化字段 + SummaryChecksum | +| 预留区 | [928, 4096) | 未来扩展,零填充 | + +### 1.1 字段定义 + +``` +═══════════════════════════════════════════════════════════════════════════════ +控制区 [0, 96) — 崩溃恢复关键字段 + ControlChecksum +═══════════════════════════════════════════════════════════════════════════════ +Offset Size Field Type Description +────── ───── ──────────────── ────────── ────────────────────────────────────────── +0 4B Magic uint32 BE 固定 0x5A534D54 ("ZSMT") +4 2B HeaderVersion uint16 BE 二进制格式版本号,当前 = 1 +6 1B ActiveSlot uint8 0 = Slot A,1 = Slot B +7 1B PendingOp uint8 0 = 无,1 = config_update,2 = storage_change +8 8B WriteSequence uint64 BE 单调递增写计数器 +16 8B SlotAOffset uint64 BE Slot A 在 LV 中的字节偏移 +24 8B SlotACapacity uint64 BE Slot A 容量(字节) +32 8B SlotBOffset uint64 BE Slot B 在 LV 中的字节偏移 +40 8B SlotBCapacity uint64 BE Slot B 容量(字节) +48 8B LastUpdateTime uint64 BE 最后成功写入的 epoch 毫秒 +56 8B SchemaVersion uint64 BE Payload JSON schema 版本(扩容后 20 bit/段) +────── +64B (以上为 ControlChecksum 覆盖范围) +────── +64 32B ControlChecksum raw bytes SHA-256(bytes[0:64]) + +═══════════════════════════════════════════════════════════════════════════════ +VM 摘要区 [96, 928) — 扫描优化字段 + SummaryChecksum +═══════════════════════════════════════════════════════════════════════════════ +96 1B VmCategory uint8 VM 类别(0=REGULAR, 1=TEMPLATE, 2=TEMPLATE_CACHE) +97 32B VmUuid ASCII VM UUID hex 字符串(32 字符,无连字符) +129 2B VmNameLen uint16 BE VmName 实际字节长度(0 表示未设置) +131 765B VmName UTF-8 VM 名称(varchar(255)×utf8,最大 765 字节) +────── +896B (以上 VM 摘要字段,[96:896) 为 SummaryChecksum 覆盖范围) +────── +896 32B SummaryChecksum raw bytes SHA-256(bytes[96:896]) + +═══════════════════════════════════════════════════════════════════════════════ +预留区 [928, 4096) — 未来扩展 +═══════════════════════════════════════════════════════════════════════════════ +928 3168B Reserved zero 零填充,未来扩展使用 + +══════ +Total: 4096B +``` + +### 1.2 字段设计理由 + +**Magic (4B, offset 0)** +- `0x5A534D54` = ASCII "ZSMT" (ZStack Metadata) +- hexdump 一眼可辨识 +- brute-force 恢复时每个 4KB 对齐位置只需读前 4 字节判断 + +**HeaderVersion (2B, offset 4)** +- 二进制布局版本,只在 Header/Slot 结构变更时递增 +- uint16 足够(不可能有 65535 次布局变更) +- 与 SchemaVersion 职责分离:HeaderVersion 管"怎么读",SchemaVersion 管"读出的 JSON 怎么解释" + +**ActiveSlot (1B, offset 6) + PendingOp (1B, offset 7)** +- 各 1B 足够(`PendingOp` 当前已定义取值 0~2) +- 不用 bit flags:语义清晰,调试简单 +- 紧凑排列,在同一个 8B 对齐块内 +- 前向兼容:预留值域 **3~255**。读取端遇到未知值时按 `STORAGE_CHANGE` 语义处理(保守路径),不做 `CONFIG_UPDATE` 快速清理,避免误判导致数据丢失 + +**PendingOp 前向兼容约束(Q4-6)** + +| 取值 | 语义 | 处理策略 | +|------|------|----------| +| 0 | NONE | 正常读取流程 | +| 1 | CONFIG_UPDATE | 可按配置变更修复策略清理 PendingOp | +| 2 | STORAGE_CHANGE | 走存储变更保守修复路径 | +| 3~255(未知) | 未来扩展保留值 | **按 STORAGE_CHANGE 处理**(保守回退) | + +> 设计理由:未知 PendingOp 若误按 CONFIG_UPDATE 清理,可能在扩容/布局切换未完成场景提前丢弃恢复机会;按 STORAGE_CHANGE 保守处理最多增加一次 full-refresh,不会扩大数据风险。 + +**WriteSequence (8B, offset 8)** +- uint64,理论上限 ~1.8×10¹⁹ +- 以 1000 次/秒计算,约 5.84 亿年溢出 +- 自然对齐在 offset 8 + +**SlotAOffset / SlotBOffset (各 8B, offset 16/32)** +- **显式存储**,不再通过 `SlotBOffset = ALIGNMENT + SlotACapacity` 间接计算 +- 消除了 SlotACapacity 修改导致 SlotB 定位错误的连锁风险 +- 恢复时直接从 raw Header 提取 offset 即可尝试读 Slot + +**SlotACapacity / SlotBCapacity (各 8B, offset 24/40)** +- uint64 最大值 16EB,远超 64MB LV 上限 +- 两个字段分开存储,为未来非对称 Slot 预留可能 + +**LastUpdateTime (8B, offset 48)** +- epoch 毫秒,uint64 +- 诊断用途:不读 Slot 即可判断最后更新时间 +- 冲突检测:多主脑裂时辅助判断数据新鲜度 + +**SchemaVersion (8B, offset 56)** +- Payload JSON 的业务 schema 版本,uint64 +- 读 Header 即可判断是否认识该版本,无需解码整个 Slot +- **编码规则**:将 `dbf.getDbVersion()` 返回的数据库版本字符串(如 `"4.10.12"`)解析为数字组件后压缩为 uint64:**`(A << 40) | (B << 20) | C`**。例如 `"4.10.12"` → `(4 << 40) | (10 << 20) | 12`。解码:`A = v >> 40`,`B = (v >> 20) & 0xFFFFF`,`C = v & 0xFFFFF`。每个组件 20 bit,最大支持 **1,048,575** +- Header 中的 SchemaVersion 与 DTO JSON 中的 `schemaVersion` 字符串(`dbf.getDbVersion()`)是同一语义的不同表示,写入时编码、读取时解码 +- 扩容理由:原 uint32 每段 10 bit(最大 1023),uint64 每段 20 bit,容量扩大一倍(bit 数),版本号空间充裕 +- **格式合约**:`dbf.getDbVersion()` 返回值必须匹配 `^\d+\.\d+\.\d+$`,每段 ≤ 1,048,575。不匹配时拒绝编码并记录 ERROR 日志 +- **Python 2 注意**:位移操作需用 long 字面量避免溢出,如 `4L << 40`。或统一使用 `int()` 包裹:`int(a) << 40 | int(b) << 20 | int(c)`。Python 2 的 `int` 在超过 `sys.maxint` 时自动提升为 `long`,但显式使用 `long()` 更安全 +- **字段迁移说明**:原控制区 Reserved(4B, offset 60) 的位置被 SchemaVersion 扩展吸收。控制区内的预留空间功能由 Header 预留区 [928, 4096)(3168B)承担,空间充裕 + +**ControlChecksum (32B, offset 64)** +- SHA-256 of bytes[0:64] +- 覆盖 Checksum 之前的所有控制字段(Magic 到 SchemaVersion,共 64B) +- **不覆盖 VM 摘要区和预留区**:职责分离,控制区和摘要区各自独立校验 +- 校验逻辑:`sha256(block[0:64]) == block[64:96]` + +**VmCategory (1B, offset 96)** +- VM Business 类别,用于批量扫描时快速分类筛选 +- 0=REGULAR(普通虚拟机,含链式克隆子 VM),1=TEMPLATE(模板 VM),2=TEMPLATE_CACHE(缓存 VM) +- 旧版本写入的 Header 此处为 0,解读为 REGULAR(向后兼容) +- 枚举值与 Java 侧 `VmMetadataCategory` 一致(见 [Part 1a §2.2](vm-metadata-01a-数据模型与序列化.md#22-vmmetadatacategory-枚举)) + +**VmUuid (32B, offset 97)** +- VM UUID 的 hex 字符串表示(32 字符 ASCII,无连字符) +- 扫描时无需解码 Slot 即可按 UUID 检索 +- 固定 32B,无需长度前缀 + +**VmNameLen (2B, offset 129)** +- VmName 字段的实际 UTF-8 字节长度 +- uint16 BE,最大 65535,远超 765B 上限 +- 0 表示未设置(旧版本兼容:旧 Header 此处为 0,意为"名称不可用,需读 Slot") + +**VmName (765B, offset 131)** +- VM 名称 UTF-8 编码,MySQL `varchar(255)` + `charset utf8` 最大 765 字节 +- **截断规则**:若 VM 名称的 UTF-8 字节数超出 765B,截断到 765B(在 UTF-8 字符边界截断,避免截断多字节字符的中间)。截断不影响 Slot 中的完整 JSON 中的 VM 名称 +- 尾部未使用空间零填充 + +**SummaryChecksum (32B, offset 896)** +- SHA-256 of bytes[96:896] +- 独立于 ControlChecksum,仅覆盖 VM 摘要字段 +- 扫描时校验失败 → 摘要不可信 → 需读 Slot 获取 VM 信息(降级但不影响数据正确性) +- 与 ControlChecksum 分离的理由:控制区是崩溃恢复的核心,不能因为摘要区写入异常导致控制区被判为无效 + +**Reserved (3168B, offset 928)** +- 零填充至 4096B +- 未来扩展空间充裕:可增加大量新字段而无需再次扩容 Header + +### 1.3 hexdump 示例 + +``` +// 控制区 [0, 64) +00000000 5a 53 4d 54 00 01 00 01 00 00 00 00 00 00 00 2a |ZSMT...........*| + ^^^^^^^^^ ^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + Magic V=1 A=0 P=1 WriteSeq = 42 + +00000010 00 00 00 00 00 00 10 00 00 00 00 00 00 1f e0 00 |................| + ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + SlotAOffset = 4096 SlotACapacity = 2088960 + +00000020 00 00 00 00 00 1f f0 00 00 00 00 00 00 1f e0 00 |................| + ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + SlotBOffset = 2093056 SlotBCapacity = 2088960 + +00000030 00 00 01 8e 3a 5b c0 00 00 00 04 00 00 a0 00 0c |....:...........| + ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + LastUpdate=1709123456000 SchemaVersion="4.10.12" + +// ControlChecksum [64, 96) +00000040 a1 b2 c3 d4 ... (32 bytes SHA-256 of [0:64]) ... |................| + +// VM 摘要区 [96, 896) +00000060 01 61 62 63 64 65 66 30 31 32 33 34 35 36 37 38 |.abcdef012345678| + ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Cat=1 VmUuid (前 15B) = "abcdef0123456789..." + +00000070 39 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 |901234567890abcde| + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + VmUuid (后 17B) = "...901234567890abcde" + +00000081 00 09 e6 b5 8b e8 af 95 56 4d 00 00 00 ... |........VM......| + ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + NameLen=9 VmName="测试VM" (UTF-8, 9 bytes) + + ... (VmName padding to offset 896) ... + +// SummaryChecksum [896, 928) +00000380 f1 e2 d3 c4 ... (32 bytes SHA-256 of [96:896]) ...|................| + +// 预留区 [928, 4096) +000003A0 00 00 00 00 ... (zero padding to 4096B) ... |................| +``` + +### 1.4 版本兼容策略 + +``` +读取时: + if header_version > MAX_KNOWN_VERSION: + → 拒绝解析,返回错误,提示升级软件 + + if header_version == 1: + → 用 V1 布局解析(当前方案) + + # 未来 V2 示例: + if header_version == 2: + → 控制区 Reserved 位置改为 CompressionType + → 预留区 offset 928~935 分配给新字段 + → ControlChecksum 范围不变(仍 bytes[0:64]) + → 新字段在预留区有各自独立校验或归入 SummaryChecksum +``` + +--- + +## 2. Slot 结构 + +Slot 是数据搬运工,职责单一:可靠地存取 payload、支持自描述恢复。 + +### 2.1 字段定义 + +``` +Offset Size Field Type Description +─────── ───── ────────────── ────────── ────────────────────────────────── +0 4B Magic uint32 BE 固定 0x5A534454 ("ZSDT") +4 8B SeqNum uint64 BE 写序号,与 Header.WriteSequence 对应 +12 8B SlotOffset uint64 BE 自描述:本 Slot 在 LV 中的字节偏移 +20 8B SlotCapacity uint64 BE 自描述:本 Slot 容量 +28 8B PayloadLen uint64 BE Payload 实际字节数 +─────── +36B (固定 Header 部分) +─────── +36 NB Payload raw bytes 元数据 DTO JSON 明文(systemTags/resourceConfigs 为 per-Resource Base64 编码) +36+N 32B Checksum raw bytes SHA-256(bytes[0:36+N]) +─────── +Total: 36 + N + 32 B +``` + +> **注**:Checksum 中 `bytes[0:36+N]` 的偏移量相对于 **Slot 起始位置**(即 `SlotOffset`),非 LV 起始位置。实现时应从 SlotOffset 开始读取 `36+N` 字节作为 Checksum 输入。 + +### 2.2 字段说明 + +| 字段 | 设计理由 | +|------|---------| +| Magic | 标识 Slot 数据块,brute-force 恢复的入口条件 | +| SeqNum | 与 Header.WriteSequence 匹配来判断 Phase 2 是否完成 | +| SlotOffset | Header 损坏时的自描述定位;brute-force 时 `stored_offset == actual_offset` 是强校验 | +| SlotCapacity | 配合 SlotOffset 可重建布局;`SlotA.Offset + SlotA.Capacity` 可定位 SlotB | +| PayloadLen | 8B (uint64),虽然实际不超过 32MB,但保持与其他字段统一的 8B 对齐 | +| Payload | 变长,元数据 DTO JSON(systemTags/resourceConfigs 字段为 per-Resource Base64 编码) | +| Checksum | 尾部放置,SHA-256 覆盖 SlotHeader + Payload 全部内容 | + +### 2.3 Checksum 放尾部的理由 + +| 方案 | 优点 | 缺点 | 结论 | +|------|------|------|------| +| Checksum 在尾部(当前) | 写入自然流程;覆盖全部数据 | 需先读 PayloadLen 才知道 Checksum 位置 | (Y) 采用 | +| Checksum 在 Header 固定位置 | 固定偏移 | 不读 Payload 也无法校验,没有实际收益 | (N) | +| Header/Payload 双 Checksum | 可先验证 Header | 增加写入复杂度,1MB 优化读已覆盖大多数场景 | (N) | + +### 2.4 校验清单 + +> 两种校验模式:正常路径 (strict) 用于常规读取,恢复路径 (relaxed) 用于 Header 损坏后的 Slot 恢复。 + +| 校验项 | strict(正常读取) | relaxed(恢复路径) | +|--------|-------------------|-------------------| +| Magic == 0x5A534454 | (Y) | (Y) | +| SlotOffset == expected | (Y) | (Y) | +| SlotCapacity == expected | (Y) | (N) 跳过(推算 capacity 可能不准) | +| PayloadLen 范围合理 | (Y) | (Y) | +| SHA-256 Checksum | (Y) | (Y) | + +### 2.5 不做修改的候选项 + +| 候选改进 | 结论 | 理由 | +|---------|------|------| +| PayloadLen 缩为 4B | (N) 不改 | 只省 4B,破坏 8B 对齐 | +| 增加 SlotIndex (A/B 标识) | (N) 不改 | SeqNum 已够判断顺序,SlotIndex 冗余 | +| 增加 Slot 独立版本号 | (N) 不改 | Header 的 HeaderVersion 已管控全局布局版本 | + +## 3. 约束与不変量 + +1. **控制区校验不変量**:`ControlChecksum = SHA-256(bytes[0:64])`,读取端必须先校验再使用控制字段。 +2. **摘要区校验不変量**:`SummaryChecksum = SHA-256(bytes[96:896])`;校验失败只影响摘要可用性,不影响控制区有效性判断。 +3. **PendingOp 安全不変量**:未知 `PendingOp`(3~255)必须按 `STORAGE_CHANGE` 保守处理,不得降级为配置类快速路径。 +4. **Slot 自描述不変量**:`SlotOffset` 与 `SlotCapacity` 必须可用于 Header 损坏场景的恢复定位。 +5. **兼容演进不変量**:新增字段优先使用 Header 预留区 [928,4096),避免破坏既有控制区校验边界。 diff --git "a/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" "b/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" new file mode 100644 index 00000000000..5bedbcf43cd --- /dev/null +++ "b/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" @@ -0,0 +1,268 @@ +# VM 元数据 — sblk 写入流程 + +## 1. 设计原则 + +1. **Phase 1 不破坏现状**:写入的 Header 保留 Active Slot 的完整定位能力(布局字段 = 旧值) +2. **Phase 3 一次性提交**:ActiveSlot 切换、布局更新、PendingOp 清除 + VM 摘要更新在同一次 4KB Header 写入中完成 +3. **Slot 自描述**:每个 Slot 内嵌位置信息,即使 Header 损坏也可恢复 +4. **崩溃安全模型**:见 Part 4a §5.3 + +--- + +## 2. op_type 决策机制 + +> op_type 由控制面指定,Agent 端直接使用。 + +管理层面调用 `writeMetadata(payload, storageStructureChange)` 时显式指定 op_type: + +| 控制面输入 | Agent 端映射 | PendingOp 值 | +|-----------|-------------|-------------| +| `storageStructureChange = false` | CONFIG_UPDATE | 1 | +| `storageStructureChange = true` | STORAGE_CHANGE | 2 | + +**控制面决策规则**: + +| 场景 | storageStructureChange | 来源 | +|------|----------------------|------| +| `@MetadataImpact(CONFIG)` API(CPU/内存/标签等) | `false` | 注解 | +| `@MetadataImpact(STORAGE)` API(磁盘挂载/卸载、快照等) | `true` | 注解 | +| Full-refresh / 首次写入 | `true` | 控制面显式指定 | +| 多次 `markDirty` 合并 | OR 升级(任一为 true 则 true) | Poller 批量处理 | + +**好处**: +- 控制面对 op_type 拥有完整语义信息(知道哪个 API 触发了变更) +- Agent 无需读取旧 payload 做 diff,减少一次 I/O +- `VmMetadataDirtyVO` 记录 `storageStructureChange` 字段,Poller 批量处理时直接使用 + +--- + +## 3. 完整流程 + +### 3.1 前置步骤 + +``` +target_slot = 1 - Header.ActiveSlot +new_seq = Header.WriteSequence + 1 + +如果 payload 超出当前 Slot 容量: + new_lv_size = calculate_extend_size(current_lv_size, required) + 执行 lvextend(详见 Part 4e §2.4) + new_layout = calculate_slot_layout(new_lv_size) +否则: + new_layout = 当前 Header 中的布局(offset + capacity 不变) +``` + +### 3.2 Phase 1 — Mark Intent (4KB Header 写入) + +``` +写入 Header(4096B): + + 控制区 [0, 64): + Magic = 0x5A534D54 (不变) + HeaderVersion = 当前版本 (不变) + ActiveSlot = 旧值 ← 不切换 + PendingOp = op_type (1 或 2) ← 标记意图 + WriteSequence = new_seq ← 递增 + SlotAOffset = 旧值 ← 不变 + SlotACapacity = 旧值 ← 不变 + SlotBOffset = 旧值 ← 不变 + SlotBCapacity = 旧值 ← 不变 + LastUpdateTime = 旧值 ← 不变 + SchemaVersion = 旧值 ← 不变 + ControlChecksum = SHA-256(bytes[0:64]) + + VM 摘要区 [96, 928):保持旧值不变(Phase 1 不更新摘要) + SummaryChecksum = 旧值 ← 不重算 + 预留区 [928, 4096):零填充 + +关键约束:布局字段(Offset/Capacity)和 VM 摘要全部保持旧值 +理由:确保崩溃后 Active Slot 的定位信息完好;摘要在 Phase 3 统一更新 +``` + +### 3.3 Phase 2 — Write Payload + +``` +目标 Slot = target_slot +使用 new_layout 中的 offset/capacity + +写入 Slot 数据: + SlotHeader: + Magic = 0x5A534454 + SeqNum = new_seq + SlotOffset = new_layout 中目标 slot 的 offset + SlotCapacity = new_layout 中目标 slot 的 capacity + PayloadLen = len(payload) + Payload: + 元数据 DTO JSON(systemTags/resourceConfigs 为 per-Resource Base64) + Checksum: + SHA-256(SlotHeader + Payload) + +写入按 ALIGNMENT(4096) 对齐,零填充 +``` + +### 3.4 Phase 3 — Commit (4KB Header 写入) + +``` +写入 Header(4096B): + + 控制区 [0, 64): + Magic = 0x5A534D54 (不变) + HeaderVersion = 当前版本 (不变) + ActiveSlot = target_slot ← 切换 + PendingOp = 0 ← 清除 + WriteSequence = new_seq ← 保持 Phase 1 值 + SlotAOffset = new_layout 值 ← 此时更新 + SlotACapacity = new_layout 值 ← 此时更新 + SlotBOffset = new_layout 值 ← 此时更新 + SlotBCapacity = new_layout 值 ← 此时更新 + LastUpdateTime = now() ← 此时更新 + SchemaVersion = 当前 schema 版本 ← 此时更新 + ControlChecksum = SHA-256(bytes[0:64]) + + VM 摘要区 [96, 928): + VmCategory = vm_category ← 此时更新 + VmUuid = vm_uuid ← 此时更新(首次写入后不变) + VmNameLen = len(vm_name_utf8) ← 此时更新 + VmName = vm_name_utf8 ← 此时更新 + SummaryChecksum = SHA-256(bytes[96:896]) + + 预留区 [928, 4096):零填充 + +关键:ActiveSlot 切换 + 布局更新 + PendingOp 清除 + VM 摘要更新 + 在同一次 4KB Header O_DIRECT 写入中完成。 +``` + +### 3.5 Header 字段变更对照表 + +| 字段 | Phase 1 | Phase 3 | +|------|---------|---------| +| Magic | 不变 | 不变 | +| HeaderVersion | 不变 | 不变 | +| ActiveSlot | **不变**(旧值) | **切换**(target) | +| PendingOp | **设置**(op_type) | **清除**(0) | +| WriteSequence | **递增**(new_seq) | 不变(保持 new_seq) | +| SlotAOffset | **不变**(旧值) | **更新**(new_layout) | +| SlotACapacity | **不变**(旧值) | **更新**(new_layout) | +| SlotBOffset | **不变**(旧值) | **更新**(new_layout) | +| SlotBCapacity | **不变**(旧值) | **更新**(new_layout) | +| LastUpdateTime | **不变**(旧值) | **更新**(now) | +| SchemaVersion | **不变**(旧值) | **更新**(当前版本) | +| ControlChecksum | 重算 | 重算 | +| VM 摘要区 | **不变** | **更新** | +| SummaryChecksum | **不变** | **重算** | + +--- + +## 4. 崩溃场景分析 + +### 4.1 崩溃分析表 + +| 崩溃点 | Header 状态 | Active Slot | Target Slot | 恢复行为 | 结果 | +|--------|------------|-------------|-------------|----------|------| +| Phase 1 之前 | 旧值,pending=0 | 有效 | 旧/空 | 正常读 Active | (Y) 读旧数据 | +| Phase 1 之后,Phase 2 之前 | pending=op, seq=new, **布局=旧** | 有效(旧布局定位正确) | 旧/空 | 用旧布局找 Target → SeqNum≠new_seq → 回退 Active | (Y) 读旧数据 | +| Phase 2 进行中 | pending=op, seq=new, **布局=旧** | 有效 | 损坏(partial write) | 用旧布局找 Target → Checksum fail → 回退 Active | (Y) 读旧数据 | +| Phase 2 完成,Phase 3 之前 (无 extend) | pending=op, seq=new, **布局=旧** | 有效 | 有效,在旧布局位置 | 用旧布局找 Target → SeqNum==new_seq → 使用新数据 | (Y) NEED_REPAIR + 读新数据(Phase 2 数据有效,需 repair 完成 Phase 3) | +| Phase 2 完成,Phase 3 之前 (有 extend) | pending=op, seq=new, **布局=旧** | 有效 | 有效,但在新布局位置 | 用旧布局找 Target → 旧位置无有效数据 → 回退 Active | (!) 读旧数据;但 repair_pending_op 双布局尝试可恢复新数据(详见 Part 4d §4.5) | +| Phase 3 之后 | 全新值,pending=0 | 新 Active 有效 | — | 正常读新 Active | (Y) 读新数据 | + +### 4.2 LV extend + 崩溃场景详细分析 + +**场景:ActiveSlot=1(B),payload 太大触发 extend** + +``` +初始状态: + LV = 4MB + SlotA: offset=4096, cap=2044KB + SlotB: offset=2MiB+4096, cap=2044KB + ActiveSlot = 1 (Slot B) + +写入操作: + target = Slot A (inactive) + extend LV → 8MB + new_layout: SlotA offset=4096, cap=4MB; SlotB offset=4MB+4096, cap=4MB + +Phase 1: 写 Header + PendingOp=op, WriteSeq=new + SlotAOffset=4096, SlotACap=2044KB ← 旧值! + SlotBOffset=2MiB+4096, SlotBCap=2044KB ← 旧值! + +Phase 2: 写 payload 到 Slot A + 使用 new_layout: offset=4096, cap=4MB + +崩溃!Phase 3 未执行 +``` + +**恢复:** +- Header 中 ActiveSlot=1 → 读 Slot B +- SlotBOffset=2MiB+4096(旧值)→ Slot B 数据在该位置 → **定位正确** (Y) +- 读到旧数据,返回 NEED_REPAIR 或 STORAGE_CHANGE_INCOMPLETE + +**对比旧方案(不修复的情况):** +- 旧方案 Phase 1 会写新 capacity → SlotBOffset = 4096+4MB → Slot B 实际数据在 2MiB+4096 → **定位失败** (N) + +### 4.3 extend 场景丢失写入的权衡 + +> **扩容场景修复**:若 Phase 1 完成后 LV 已扩容且 Phase 2 已用新布局写入,repair_pending_op 需尝试双布局恢复。详见 [Part 4d §repair](vm-metadata-04d-sblk读取与恢复.md#repair_pending_op)。 + +**丢失发生条件(必须同时满足):** +1. 本次写入触发了 LV extend +2. 崩溃恰好发生在 Phase 2 完成后、Phase 3 执行前 + +**为什么可以接受:** +- 数据安全:旧数据完整可读,不损失已提交数据 +- 语义正确:Phase 3 未完成 = 事务未提交 = 丢弃未提交数据是正确行为 +- 自动恢复:management plane 检测到 pending_op 后会重试或 repair +- 概率极低:extend 不频繁(4MB→64MB 最多几次),且崩溃恰好卡在极窄窗口 + +**替代方案评估:** + +| 方案 | 可行性 | 问题 | +|------|--------|------| +| Phase 2 写入旧布局位置 | (N) | 旧容量不够(否则不需要 extend) | +| 四阶段写入(Phase 2.5 更新布局) | (N) | Phase 2.5 崩溃后回到同样问题 | +| Write Ahead Log | (N) | 过度设计,复杂度与收益不对等 | + +**结论:接受此场景下的行为,三阶段足够。** + +--- + +## 5. 完整状态转换图 + +``` + ┌──────────────┐ + │ PendingOp=0 │ 正常状态 + │ ActiveSlot=X│ + └──────┬───────┘ + │ + write_metadata() + │ + ┌────────────▼────────────┐ + │ Phase 1 │ + │ PendingOp=1或2 │ + │ WriteSeq=new │ + │ ActiveSlot=X (不变) │ + │ Layout=旧 (不变) │ + └────────────┬─────────────┘ + │ + ┌──────▼──────┐ + ┌───────│ Phase 2 │───────┐ + │ │ Write Slot │ │ + │ └──────┬──────┘ │ + │ │ │ + 崩溃(Target无效) 崩溃(Target有效) 正常 + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌───────────┐ ┌──────────────┐ + │回退到旧 │ │NEED_REPAIR│ │ Phase 3 │ + │Active │ │可用新数据 │ │ Commit │ + └────┬─────┘ └─────┬─────┘ └──────┬───────┘ + │ │ │ + ┌────▼─────┐ ┌─────▼─────┐ ┌──────▼───────┐ + │若op=1: │ │ repair: │ │ PendingOp=0 │ + │清除→OK │ │完成Phase3 │ │ ActiveSlot=Y │ + │若op=2: │ │ │ │ Layout=新 │ + │不清除→ │ └───────────┘ └──────────────┘ + │需refresh │ 正常状态 + └──────────┘ +``` diff --git "a/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" "b/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" new file mode 100644 index 00000000000..8edc2091ff6 --- /dev/null +++ "b/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" @@ -0,0 +1,441 @@ +# VM 元数据 — sblk 读取与恢复 + +## 1. 读取主流程 + +``` +read_metadata(lv_path, lv_size): + + 1. 以 O_DIRECT | O_SYNC 只读打开 LV + 2. 读 Header Block (4KB) + 3. 反序列化 + 校验 Header(magic、version、ControlChecksum) + + 4. 如果 Header 有效: + 从 Header 直接读取 Slot 定位信息: + slot_a_off = Header.SlotAOffset ← 显式读取,不计算 + slot_a_cap = Header.SlotACapacity + slot_b_off = Header.SlotBOffset ← 显式读取,不计算 + slot_b_cap = Header.SlotBCapacity + + 根据 PendingOp 分支 → Flow A / B / C + + 5. 如果 Header 无效: + → 进入恢复流程(§3) +``` + +--- + +## 2. 三种读取分支 + +### 2.1 Flow A — PendingOp = 0(正常) + +``` +读 Active Slot → 校验通过 → 返回 OK + payload + +如果 Active Slot 校验失败: + 读 Inactive Slot + 如果 Inactive 有效: + → DEGRADED + payload(inactive) + → is_usable = True(允许灾备注册) + → warning: "Active 损坏,已降级使用 Inactive,数据可能落后一写入周期" + → repair_action: 后台触发 repair(优先切换 Active;必要时 full-refresh) + 如果 Inactive 也无效: + → CORRUPTED + "两个 Slot 均损坏" + → repair_action: full-refresh +``` + +### 2.2 Flow B — PendingOp = 1(CONFIG_UPDATE 中断) + +CONFIG_UPDATE 的特点:旧数据可以安全使用(只是配置过时,不会导致数据损坏)。 + +``` +target_slot = 1 - ActiveSlot + +尝试读 Target Slot: + 如果有效 且 SeqNum == Header.WriteSequence: + → Phase 2 已完成,Phase 3 未完成 + → NEED_REPAIR + target 的 payload(更新的数据) + → repair_action: 完成 Phase 3 + + 否则(Target 无效或 SeqNum 不匹配): + 回退读 Active Slot: + 如果有效: + → NEED_REPAIR + active 的 payload(旧但安全的数据) + → repair_action: 清除 PendingOp + 如果无效: + → CORRUPTED +``` + +### 2.3 Flow C — PendingOp = 2(STORAGE_CHANGE 中断) + +STORAGE_CHANGE 的特点:存储操作可能已在块设备层面完成,旧元数据描述的存储拓扑与实际不符,**使用旧元数据注册 VM 可能导致数据丢失**。 + +``` +target_slot = 1 - ActiveSlot + +尝试读 Target Slot: + 如果有效 且 SeqNum == Header.WriteSequence: + → Phase 2 已完成,Phase 3 未完成 + → NEED_REPAIR + target 的 payload(新拓扑数据) + → repair_action: 完成 Phase 3 + → 这是安全的,新数据反映了存储变更 + + 否则(Target 无效或 SeqNum 不匹配): + → Phase 2 未完成或数据损坏 + → 旧 Active Slot 的数据已过期,不反映当前存储状态 + + 读 Active Slot(仅用于诊断,标记为 stale): + → STORAGE_CHANGE_INCOMPLETE + → payload = active 的旧数据(标记为 stale) + → is_usable() = False ← 关键:禁止正常使用 + → error: "存储拓扑已变更但元数据未更新,必须执行 full-refresh" + → repair_action: "从数据库重建元数据,执行 full-refresh" +``` + +> **H2 修复 — 管理面自愈链路**:当控制面收到 `STORAGE_CHANGE_INCOMPLETE` 状态时,执行 `markDirty(vmUuid, true)`(`storageStructureChange=true`)触发 Poller 全量重建。若 Poller 重试耗尽,dirty 行被删除的同时在 `VmMetadataPathFingerprintVO` 上标记 `lastFlushFailed=true`。独立的 `MetadataStaleRecoveryTask`(每 30 分钟)扫描该标记并重新 `markDirty()`,为低频 VM 提供持续自愈能力。详见 [Part 2 §4.8](vm-metadata-02-脏标记与Poller.md#48-stale-恢复任务h2-修复)。 + +### 2.4 ReadResult 状态语义 + +| Status | payload | is_usable() | 调用方行为 | +|--------|---------|-------------|-----------| +| OK | (Y) 有效 | True | 正常使用 | +| NEED_REPAIR | (Y) 有效 | True | 使用数据 + 触发后台 repair | +| RECOVERED | (Y) 有效 | True | 使用数据 + 触发 Header 重建 | +| DEGRADED | (!) 有效(非最新) | True | 允许继续(如灾备注册)+ 必须告警 + 触发 repair | +| STORAGE_CHANGE_INCOMPLETE | (!) stale 数据 | **False** | **禁止注册 VM**,必须 full-refresh | +| CORRUPTED | (N) 无 | False | 必须 full-refresh | + +### 2.5 Slot 读取优化 + +``` +optimistic_read_size = min(slot_capacity, 1MB) + +第一次读: 从 slot_offset 读 optimistic_read_size + → 大多数情况下 payload < 1MB,一次读取完成 + +如果 payload + header + checksum > optimistic_read_size: + 第二次读: 从 slot_offset 读 aligned_up(total_needed) + → 仅在极大 payload 时触发 +``` + +--- + +## 3. Header 损坏恢复 + +当 Header 校验失败(magic 错误、checksum 不匹配、version 不认识)时,进入分层恢复。 + +### 3.1 恢复层次总览 + +``` +Layer 1: Raw Header 字段提取 + │ 即使 ControlChecksum 校验失败,Header 控制区 [0, 64) 的字段可能仍可读 + │ 尝试提取 ActiveSlot、SlotAOffset、SlotBOffset 等 + │ 比无 Offset 字段时多了直接定位信息,恢复成功率更高 + │ + ▼ +Layer 2: 布局推算 + │ 用 `blockdev --getsize64 /dev/{vg}/{lv}` 获取当前 LV 实际大小 + │ 先尝试当前 lv_size 对应布局,再穷举 KNOWN_LV_SIZES 的历史布局 + │ KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] + │ 每个布局最多探测 A/B 两个 Slot(最多 18 次 I/O 探测) + │ 命中任一可校验 Slot 即返回该布局候选 + │ + ▼ +Layer 3: Slot A 自描述辅助定位 Slot B + │ 如果 Layer 2 的 Slot B 位置失败 + │ 从 Slot A 的 SlotOffset + SlotCapacity 推算旧 Slot B 位置 + │ 覆盖 LV extend 后布局变化的情况 + │ + ▼ +Layer 4: Brute-force 扫描 + 最后手段,以 1MB 为单位批量读取 LV,在内存中逐 ALIGNMENT 对齐位置搜索 + 匹配条件:ZSDT Magic + SlotOffset == actual_offset(双重校验,误报极低) + 64MB LV ≈ 64 次 × 1MB 读 ≈ 64MB I/O(顺序读,SSD 场景 <1s) +``` + +### 3.2 Slot 选择策略 + +当找到两个有效 Slot 时: + +``` +优先级: + 1. Raw Header 中的 ActiveSlot hint(如果可提取)→ 使用 hint 指向的 Slot + 2. 无 hint → 使用 SeqNum 更高的 Slot(最后写入的数据更新) + 3. 只有一个有效 → 使用该 Slot + 4. 都无效 → CORRUPTED + +注意:恢复路径使用 relaxed 校验模式(见 Part 4b §2.4) + - 不校验 SlotCapacity(因为传入的 capacity 可能是推算的,与 Slot 自描述不同) + - 依赖 Checksum 作为最终数据完整性裁判 +``` + +### 3.3 Layer 2 多布局穷举(Q4-1) + +``` +KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] + +输入: current_lv_size +候选集合: [size in KNOWN_LV_SIZES where size <= current_lv_size] + +对每个候选 size: + 1. layout = calculate_slot_layout(size) + 2. 尝试读取 slotA/slotB 的 slot header magic + checksum + 3. 若任一 slot 可校验通过,记录该 layout 为可用候选 + +选择策略: + - 优先命中 Header hint 指向的 slot/layout + - 否则按 SeqNum 选择更新数据 + +复杂度上界: + - 候选布局最多 9 个 + - 每布局最多 2 次 slot 探测 + - 总探测 ≤ 18 次 I/O(不含最终 payload 读取) +``` + +### 3.4 Layer 1 详细逻辑 + +``` +读取 Header 原始 4KB 数据(控制区字段在 [0, 64) 内) + +尝试解析 Magic: + if magic != 0x5A534D54 → 跳过 Layer 1,进入 Layer 2 + +Magic 正确但 Checksum 错误(单 bit 翻转等场景): + 提取各字段作为 hint: + active_slot_hint ← 如果值 ∈ {0, 1} 则可信 + slot_a_off_hint ← 如果值 > 0 且 < lv_size 则可用 + slot_b_off_hint ← 如果值 > slot_a_off_hint 且 < lv_size 则可用 + + 用 hint 的 offset 尝试读 Slot: + 如果成功 → 返回 RECOVERED + 如果失败 → 继续 Layer 2 +``` + +**Layer 1 的改进**:Header 显式存储 SlotAOffset + SlotBOffset,raw 提取后直接可用,无需从 SlotACapacity 间接推算,减少一步出错风险。 + +--- + +## 4. PendingOp 语义与 Repair 策略 + +### 4.1 PendingOp 语义对照 + +| PendingOp | 含义 | 写入中断的后果 | 旧数据安全性 | 可否简单清除 | +|-----------|------|--------------|-------------|-------------| +| 0 | 空闲,上次写入已完成 | — | — | — | +| 1 (CONFIG_UPDATE) | 正在写入普通配置变更的元数据 | 丢失一次配置更新,可接受 | (Y) 旧配置安全可用 | (Y) 可以 | +| 2 (STORAGE_CHANGE) | 正在写入存储变更后的元数据(存储上已有新快照/卷) | 存储上有新数据,但元数据没记录! | (N) 旧拓扑与实际不符 | (N) **绝不可以** | + +**核心区别**:CONFIG_UPDATE 的旧数据"过时但安全",STORAGE_CHANGE 的旧数据"过时且危险"。 + +### 4.2 repair_pending_op — CONFIG_UPDATE (pending_op = 1) + +``` +读取 Header → 确认 pending_op = 1 + +先用 Header 旧布局计算 target_slot 并读取 + +如果旧布局失败,再用当前 LV 大小推导新布局重试 target_slot +(双布局尝试:old-layout → new-layout) + +Case A: Target 有效 且 SeqNum == Header.WriteSequence + → Phase 2 已完成,只需完成 Phase 3 + → 写入新 Header: + ActiveSlot = target_slot + PendingOp = 0 + WriteSequence = 保持 + 布局字段 = 若命中旧布局则保持旧值,若命中新布局则更新为新布局 + LastUpdateTime = now() + → 返回 repaired=True, "Completed Phase 3" + +Case B: Target 无效(旧/新布局均失败) + → Phase 2 未完成(或数据损坏) + → 安全丢弃本次写入,恢复到旧状态 + → 写入新 Header: + ActiveSlot = 保持(旧值) + PendingOp = 0 ← 清除 + WriteSequence = 保持 + 布局字段 = 保持 + LastUpdateTime = 保持 + → 返回 repaired=True, "Aborted incomplete config update" +``` + +### 4.3 repair_pending_op — STORAGE_CHANGE (pending_op = 2) + +``` +读取 Header → 确认 pending_op = 2 + +先用 Header 旧布局计算 target_slot 并读取 + +如果旧布局失败,再用当前 LV 大小推导新布局重试 target_slot +(双布局尝试:old-layout → new-layout) + +Case A: Target 有效 且 SeqNum == Header.WriteSequence + → Phase 2 已完成,可以安全完成 Phase 3 + → 写入新 Header: + ActiveSlot = target_slot + PendingOp = 0 + WriteSequence = 保持 + 布局字段 = 若命中新布局则更新为新布局,否则保持旧值 + LastUpdateTime = now() + → 返回 repaired=True, "Completed Phase 3 for storage change" + +Case B: Target 无效(旧/新布局均失败) + → Phase 2 未完成 + → 旧 Active Slot 中的元数据不反映当前存储状态 + → (!) 不清除 PendingOp ← 关键决策 + → 返回 repaired=False, + error="STORAGE_CHANGE pending, target data lost. + Metadata is stale. Must execute full-refresh + from database to rebuild metadata." +``` + +### 4.4 为什么 STORAGE_CHANGE 不能简单清除 PendingOp + +``` +如果清除 pending_op: + Header 变为: pending=0, ActiveSlot=旧 + 后续 read_metadata → 返回 OK + 旧 payload + 调用方认为数据有效 → 用旧拓扑注册 VM + + 但实际存储状态已变更(如:快照已创建/删除、卷已扩容) + 旧拓扑 ≠ 当前存储 → VM 挂载错误的快照链 + → 数据损坏或丢失 +``` + +**PendingOp=2 是一个"脏标记"**:它的存在持续提醒系统"存储状态与元数据不一致"。只有两种方式可以消除该标记: + +1. **找到有效 Target 完成 Phase 3** — 新元数据反映了存储变更,安全 +2. **Full-refresh 写入全新元数据** — 从数据库重建完整拓扑,覆盖整个 Header + +### 4.5 双布局 repair 伪代码(Q4-5) + +```python +def repair_pending_op(header, current_lv_size): + target = 1 - header.active_slot + + # 尝试 1:Header 旧布局 + old_layout = header.layout + slot = try_read_target(target, old_layout) + if slot.valid and slot.seq_num == header.write_sequence: + return complete_phase3(header, slot, old_layout) + + # 尝试 2:当前 LV 新布局 + new_layout = calculate_slot_layout(current_lv_size) + slot = try_read_target(target, new_layout) + if slot.valid and slot.seq_num == header.write_sequence: + return complete_phase3(header, slot, new_layout) # 同步更新 Header 布局字段 + + # 双布局均失败 + if header.pending_op == STORAGE_CHANGE: + return RepairResult(repaired=False, keep_pending=True) + else: + return clear_pending_and_keep_active(header) +``` + +--- + +## 5. Full-Refresh 机制 + +### 5.1 触发条件 + +| 场景 | 触发方 | +|------|--------| +| STORAGE_CHANGE_INCOMPLETE | management plane 检测到后主动触发 | +| CORRUPTED(两个 Slot 都损坏) | management plane 检测到后主动触发 | +| repair_pending_op 返回 repaired=False | management plane 收到失败回调后触发 | +| 管理员手动触发 | 运维命令 | + +### 5.2 执行方式 + +Full-refresh 本质上是一次普通的 `write_metadata` 调用: + +``` +full_refresh(lv_path, lv_size_getter, lv_extend_func): + + 1. Management plane 从数据库查询 VM 的完整存储拓扑 + 2. 生成最新的 payload JSON + 3. 调用 write_metadata(lv_path, payload, storageStructureChange=True) + → 控制面显式指定 op_type = STORAGE_CHANGE (2) + + 写入流程: + Phase 1: PendingOp=2, WriteSeq=old+1 + Phase 2: 写入新 payload 到 inactive Slot + Phase 3: ActiveSlot 切换, PendingOp=0 + + 成功后: + - 旧的 STORAGE_CHANGE pending 状态被覆盖 + - 新元数据反映数据库中的最新拓扑 + - 两个 Slot 中至少有一个包含正确数据 +``` + +### 5.3 Full-refresh 使用 STORAGE_CHANGE(2) 的理由 + +- Full-refresh 由控制面触发,显式指定 `storageStructureChange=true` → op_type=2 +- 这自然解决了"full-refresh Phase 1 覆盖脏标记"问题:新的 PendingOp=2 与旧的语义一致 +- 不需要引入新的 OP_FULL_REFRESH (3) + +### 5.4 Full-refresh 中断场景 + +``` +如果 full-refresh 本身在 Phase 2 之前崩溃: + Phase 1 写入了 PendingOp=2 + Target 无效 + repair → Case B for STORAGE_CHANGE → 返回 STORAGE_CHANGE_INCOMPLETE + 此时 Active Slot 仍然是旧的 + + 是否有风险? + → management plane 知道 full-refresh 失败了 + (write_metadata 会抛异常),会重试。 + → 重试仍会使用 op=2(控制面显式指定),PendingOp 语义一致。 + → 只要 management plane 正确实现重试逻辑,不会误用旧数据。 +``` + +--- + +## 6. 部分写入安全性分析 + +> 完整分析见 [Part 4a §5.3](vm-metadata-04a-sblk存储协议概述.md#53-崩溃安全模型)。本节仅补充读取/恢复视角的关键结论。 + +本协议**不依赖单次 4KB I/O 的原子性**。即使 Header 的 4KB 写入在中途崩溃导致部分字段更新: + +- **ControlChecksum 不匹配** → 进入 Header 损坏恢复流程(§3) +- **Layer 1**:从 raw Header 提取 Slot 偏移量(Magic 正确时各字段大概率可读) +- **Layer 2**:从 `blockdev --getsize64` 获取的 `lv_size` 推算 Slot 布局 +- **兜底**:Slot 自描述 + SHA-256 Checksum 保证最终数据完整性 + +> 最坏情况(Header + 一个 Slot 都损坏)下,恢复流程仍能通过 Brute-force 扫描(Layer 4)找到另一个有效 Slot。 + +--- + +## 附录 A. 读取与恢复测试矩阵 + +| # | 场景 | Header 状态 | PendingOp | Active Slot | Inactive Slot | 预期结果 | +|---|------|-------------|-----------|-------------|---------------|----------| +| 1 | 正常读取 | 有效 | 0 | 有效 | — | OK + payload | +| 2 | Active 损坏(降级路径) | 有效 | 0 | 无效 | 有效 | DEGRADED + payload(inactive), is_usable=True | +| 3 | 两 Slot 损坏 | 有效 | 0 | 无效 | 无效 | CORRUPTED | +| 4 | CONFIG Phase 2 完成 | 有效 | 1 | 有效(旧) | 有效 + SeqMatch | NEED_REPAIR + 新 payload | +| 5 | CONFIG Phase 2 未完成 | 有效 | 1 | 有效(旧) | 无效 | NEED_REPAIR + 旧 payload | +| 6 | STORAGE Phase 2 完成 | 有效 | 2 | 有效(旧) | 有效 + SeqMatch | NEED_REPAIR + 新 payload | +| 7 | STORAGE Phase 2 未完成 | 有效 | 2 | 有效(旧) | 无效 | STORAGE_CHANGE_INCOMPLETE | +| 8 | Header Checksum 错误 | 无效 | — | 有效 | 有效 | RECOVERED (Layer 1/2) | +| 9 | Header Magic 错误 | 无效 | — | 有效 | — | RECOVERED (Layer 2/4) | +| 10 | 全新 LV(刚初始化) | 有效 | 0 | 有效(空 `{}`) | 零 | OK + `{}` | +| 11 | LV 扩容后旧 Header | 有效 | 0 | 有效 | — | OK(Slot offset 从 Header 读取,不依赖 lv_size) | +| 12 | 灾备复制中途快照 | 可能部分 | — | 有效 | 半写 | RECOVERED / NEED_REPAIR | +| 13 | Layer 2 多布局命中历史 4MB 布局 | Header 无效 | — | 旧布局有效 | — | RECOVERED(枚举命中,≤18 次探测) | +| 14 | Layer 2 当前布局失败→Layer 3 自描述成功 | Header 无效 | — | SlotA 有效 | SlotB 旧偏移 | RECOVERED(Layer 3) | +| 15 | repair_pending_op: old-layout 失败、new-layout 成功 | 有效 | 1/2 | 旧布局 target 无效 | 新布局 target 有效 + SeqMatch | repaired=True;若 pending=2 同步更新布局字段 | +| 16 | brute-force 扫描超时 | Header 无效 | — | 未定位 | 未定位 | CORRUPTED(返回超时错误,避免长时间阻塞) | +| 17 | extend+Phase 2 完成+初始读取(旧布局 Header) | 有效 | 1/2 | 有效(旧) | 有效但在新布局位置 | 回退 Active → NEED_REPAIR(op=1) / STORAGE_CHANGE_INCOMPLETE(op=2);后续 repair 双布局尝试可恢复 | + +--- + +## 7. 约束与不変量 + +| 约束 ID | 约束描述 | 违反后果 | 检查点 | +|---------|----------|----------|--------| +| C-RD | Flow A 中 Active 损坏且 Inactive 可校验时,必须返回 `DEGRADED` 且 `is_usable=True` | 可恢复数据被误判为不可用,灾备恢复失败 | §2.1 / §2.4 | +| C-RC | `STORAGE_CHANGE_INCOMPLETE` 必须保持 `is_usable=False`,禁止注册路径消费 | 用 stale 拓扑注册 VM,可能造成数据损坏 | §2.3 / §2.4 | +| C-SC | `STORAGE_CHANGE_INCOMPLETE` 必须通过 `markDirty(vmUuid, true)` 触发全量重建,且重试耗尽后由 `MetadataStaleRecoveryTask` 接管恢复,禁止静默放弃 | 低频 VM 存储拓扑与元数据永久不一致 | §2.3 / Part 2 §4.8 | +| C-RP | `repair_pending_op` 必须按 old-layout → new-layout 双布局尝试;pending=2 双失败时不得清除 PendingOp | extend+Phase2 完成场景丢失可恢复数据,或错误掩盖存储变更脏态 | §4.3 / §4.5 | +| C-SV | Layer 2 多布局穷举集合固定为 9 种 `KNOWN_LV_SIZES`,探测上界 ≤18 次 I/O | 恢复复杂度失控或遗漏历史布局导致恢复失败 | §3.1 / §3.4 | diff --git "a/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" "b/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" new file mode 100644 index 00000000000..c6c82370d99 --- /dev/null +++ "b/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" @@ -0,0 +1,432 @@ +# VM 元数据 — sblk 运维与 I/O 细节 + +## 1. LV 命名与扫描 + +### 1.1 命名规范 + +``` +格式: {vm_uuid}_vmmeta +示例: a1b2c3d4e5f6_vmmeta +路径: /dev/{vg_uuid}/{vm_uuid}_vmmeta +``` + +### 1.2 扫描规则 + +``` +scan_metadata_lvs(vg_path, lv_list_func): + 遍历 VG 中所有 LV + 筛选 lv_name.endswith('_vmmeta') + 返回 [{vm_uuid, lv_path, lv_size}, ...] +``` + +**大规模扫描优化**(VM 数量 > 500 时建议启用): + +| 优化手段 | 说明 | +|----------|------| +| 仅读 Header 4KB | 扫描阶段只读 Header 获取 VM 摘要(`VmUuid`/`VmName`/`VmCategory`),不读 Slot payload,单次 I/O = 4KB | +| 并行 I/O | 多个 LV 的 Header 读取可并行执行(线程池并发度受 Agent 线程数控制,默认 10) | +| SummaryChecksum 降级 | 摘要校验失败时标记 `summary_valid=false`,不在扫描阶段触发 Slot 读取 | + +--- + +## 2. LV 容量管理 + +### 2.1 基本参数 + +| 参数 | 值 | 说明 | +|------|-----|------| +| 初始大小 | 4 MB | 足够绝大多数 VM 配置 | +| 最大大小 | 64 MB | 防止单 VM 元数据占用过多空间 | +| 对齐粒度 | 4096 B (ALIGNMENT) | 满足 O_DIRECT 对齐要求 | + +### 2.2 空间分配公式 + +``` +calculate_slot_layout(lv_size): + + header_reserved = ALIGNMENT (4096 B) ← Header Block = 4KB + available = lv_size - header_reserved + slot_capacity = (available / 2) 向下对齐到 ALIGNMENT + + slot_a_offset = header_reserved ← 固定 4096 + slot_a_capacity = slot_capacity + slot_b_offset = header_reserved + slot_capacity + slot_b_capacity = slot_capacity + +示例 (4MB LV): + header_reserved = 4096 + available = 4194304 - 4096 = 4190208 + slot_capacity = (4190208 / 2) 对齐 = 2093056 (≈ 2044 KB) + slot_a_offset = 4096 + slot_b_offset = 4096 + 2093056 = 2097152 +``` + +Slot 最大 payload = slot_capacity - 36 (SlotHeader) - 32 (Checksum) = slot_capacity - 68 + +### 2.3 阶梯扩容策略 + +当 payload 超出当前 Slot 容量时触发 LV 扩容。 + +#### 扩容步长 + +| 当前 LV 大小 | 步长 | +|-------------|------| +| < 8 MB | 2 MB | +| 8 MB ~ 16 MB | 4 MB | +| 16 MB ~ 32 MB | 8 MB | +| > 32 MB | 16 MB | + +#### 设计理由 + +- 小 LV 用小步长:避免浪费(大多数 VM 的元数据在 4MB 内就够了) +- 大 LV 用大步长:减少扩容次数(快照链很长的 VM 需要更多空间) +- 最大 64MB 上限:超过说明 VM 快照/卷数量异常,应在管理层面限制 + +#### 计算示例 + +``` +场景: 当前 LV=4MB, 需要 slot 容量 3MB + +required_lv = ALIGNMENT + 2 * align_up(3MB + 68B) ≈ 6MB + 4KB +当前 4MB < required 6MB + +step 1: 4MB + 2MB = 6MB → 仍 < 6MB+4KB +step 2: 6MB + 2MB = 8MB → 满足 +→ extend LV to 8MB +``` + +### 2.4 扩容时机与交互 + +``` +write_metadata() 中: + + required = SLOT_HEADER_SIZE + len(payload) + CHECKSUM_SIZE (= 36 + N + 32) + target_cap = Header 中 target slot 的 capacity + + if required > target_cap: + min_lv = ALIGNMENT + 2 * align_up(required, ALIGNMENT) + new_lv = calculate_extend_size(current_lv_size, min_lv) + lv_extend_func(new_lv) + 重新计算 new_layout +``` + +**扩容与三阶段写入的交互**(崩溃安全分析见 Part 4c §4.2): + +``` +关键:lvextend 后必须关闭并重新打开 fd + → 确保内核重新读取块设备大小,新增空间对后续 pwrite 可见 + → close(fd) → fd = open(lv_path, O_RDWR | O_DIRECT | O_SYNC) + +布局更新时序: + 扩容后计算 new_layout(新的 offset/capacity) + Phase 1: Header 中 布局字段 = 旧值(不更新) + Phase 2: payload 写入 new_layout 的 target 位置 + Phase 3: Header 中 布局字段 = new_layout(此时更新) +``` + +### 2.5 容量超限处理 + +``` +如果 required_lv > MAX_LV_SIZE (64MB): + → 抛出异常 + → 提示 "VM 元数据超过 64MB 上限,可能快照/卷数量异常" + → 管理层面应限制: + - 单 VM 快照数量上限 + - 定期清理过期快照 + - 合并快照链 +``` + +--- + +## 3. LV 生命周期 + +### 3.1 LV 初始化 + +> **设计变更**:LV 初始化时同时写入 Header + 空 Slot A(`payload="{}"`),并执行 O_DIRECT sanity check。 + +```python +def initialize_metadata_lv(lv_path, lv_size): + fd = os.open(lv_path, os.O_RDWR | os.O_DIRECT | os.O_SYNC) + try: + # Step 0: O_DIRECT sanity check + _io_sanity_check(fd) + + layout = calculate_slot_layout(lv_size) + + # Step 1: Build empty payload Slot A + empty_payload = b'{}' + slot_a = build_slot( + magic=SLOT_MAGIC, + seq_num=1, + slot_offset=layout.slot_a_offset, + slot_capacity=layout.slot_a_capacity, + payload=empty_payload + ) + + # Step 2: Write Slot A + write_aligned(fd, layout.slot_a_offset, slot_a) + + # Step 2.5: Clear Slot B region (zero-fill) + # 确保初始化后 Slot B 为全零,避免残留数据干扰恢复流程判断 + zero_buf = b'\x00' * layout.slot_b_capacity + write_aligned(fd, layout.slot_b_offset, zero_buf) + + # Step 3: Write Header (ActiveSlot=0, WriteSequence=1, PendingOp=0) + header = build_header( + active_slot=0, pending_op=0, write_sequence=1, + slot_a_offset=layout.slot_a_offset, + slot_a_capacity=layout.slot_a_capacity, + slot_b_offset=layout.slot_b_offset, + slot_b_capacity=layout.slot_b_capacity, + last_update_time=0, schema_version=0 + ) + write_aligned(fd, 0, header) + finally: + os.close(fd) +``` + +**O_DIRECT sanity check**: + +```python +def _io_sanity_check(fd): + """Verify O_DIRECT I/O path works correctly. + + 注意:sanity check 将测试数据写入 offset 0,后续 initialize_metadata_lv() + 会在同一位置写入正式 Header,自然覆盖测试数据。如果 Header 写入失败, + offset 0 处残留测试数据(Magic ≠ 0x5A534D54),读取时将进入恢复流程 + (预期行为:初始化未完成 = 无有效元数据)。 + """ + test_data = b'ZSMT_IO_CHECK' + b'\x00' * (4096 - 13) + with AlignedBuffer(4096) as buf: + buf.fill(test_data) + buf.pwrite(fd, 0) + with AlignedBuffer(4096) as buf: + buf.pread(fd, 0) + if buf.read(13) != b'ZSMT_IO_CHECK': + raise MetadataIOError("O_DIRECT sanity check failed on %s" % lv_path) +``` + +- sanity check 失败 → 抛异常 → 管理层面将此 PS 标记为"不支持元数据" → 该 PS 上所有 VM 静默跳过元数据写入 +- 首次 `read_metadata()` → 返回 `OK` with `payload="{}"`(而非 `CORRUPTED`),避免首次读取返回 CORRUPTED 与真正损坏混淆 + +### 3.2 LV 删除 + +``` +delete_metadata(lv_path, lv_delete_func): + 直接调用 lv_delete_func(lv_path) 删除整个 LV + 无需清理内部数据 +``` + +--- + +## 4. 健康检查 + +``` +get_metadata_status(lv_path): + 只读打开 LV → 读 Header 4KB → 校验 → 返回摘要 + + 返回值: + { + valid: bool + header_version: int + active_slot: int + pending_op: int + write_sequence: int + slot_a_offset: int + slot_a_capacity: int + slot_b_offset: int + slot_b_capacity: int + last_update_time: int + schema_version: int + vm_category: int + vm_uuid: str + vm_name: str + summary_valid: bool ← SummaryChecksum 校验结果 + } + + 用途: + - 运维巡检 + - 监控告警(pending_op != 0 持续时间过长) + - 诊断工具展示 +``` + +### 4.1 Layer 4 brute-force 扫描超时与日志(Q4-2) + +当 Header/常规布局恢复失败后,Layer 4 进入按步长扫描 Slot Magic 的 brute-force 路径。为避免底层 I/O 异常导致长时间阻塞,增加**全局 30 秒超时**与启动日志: + +```python +# 在 brute-force 扫描前 +BRUTE_FORCE_TIMEOUT_SEC = 30 +start_time = time.time() +log.info("Starting brute-force scan: LV size=%dMB, max_steps=%d", lv_size_mb, max_steps) + +# 在每次 pread 循环中检查 +if time.time() - start_time > BRUTE_FORCE_TIMEOUT_SEC: + log.error("Brute-force scan timed out after %ds", BRUTE_FORCE_TIMEOUT_SEC) + return CORRUPTED +``` + +实现约束: +- 该超时为**扫描全局超时**,不是单次 `pread` 超时。 +- 超时后直接返回 `CORRUPTED`,由上层走 full-refresh / 人工介入流程。 +- 日志中的 `lv_size_mb` 与 `max_steps` 必须在扫描开始时一次性打印,便于运维关联慢盘与异常设备。 + +--- + +## 5. I/O 技术细节 + +### 5.1 字节序 + +所有多字节整数字段统一使用**大端序(Big Endian)**。Python 2 中使用 `struct.pack('>I', magic)` / `struct.pack('>H', version)` / `struct.pack('>Q', seq_num)` 等。 + +选择大端序的理由: +- 大端序是网络字节序,跨平台数据交换的惯例 +- Magic Number `0x5A534D54` 在大端序下直接对应 ASCII "ZSMT",便于 hexdump 调试 +- LVM 元数据本身也使用大端序 + +### 5.2 SHA-256 输出格式 + +使用 **32 字节二进制**(非 64 字符十六进制字符串)。Python 2 中 `hashlib.sha256(data).digest()` 得到 32 字节 bytes。 + +### 5.3 O_DIRECT 与并发控制 + +**所有 sblk 元数据读写统一使用 `O_DIRECT | O_SYNC`**,包括 Header 和 Slot。理由: + +- sblk 是共享块设备,多节点可能访问同一 LV,page cache 会导致不一致 +- 元数据操作频率低(每次 API 后一次),性能开销可忽略 +- 代码路径统一,减少 bug 风险 +- 10MB O_DIRECT 顺序写入延迟:SSD 场景约 50ms,SAN 场景约 200ms,可接受 + +**前提条件:** +- Header 从 offset 0 开始,4KB 对齐 +- 使用 O_DIRECT + O_SYNC 确保不被 page cache 缓存(共享块设备多节点访问要求) +- 如果 LVM 能在该存储上正常工作(O_DIRECT 路径可用),则崩溃安全前提已满足 + +### 5.4 文件锁 + +**sblk 不使用文件锁**:共享块设备上 `fcntl.flock` 语义取决于具体实现(device-mapper + cluster),不可靠。sblk 场景的并发保护完全依赖管理平面的四层串行化机制(见 Part 2 §3.1)。即使毫秒级窗口的并发写入,因全量覆盖写语义,后者覆盖前者,结果依然正确(最终一致性)。无需引入额外分布式锁机制。 + +> local/NFS 使用 `fcntl.flock(fd, LOCK_EX | LOCK_NB)` 作为 defense-in-depth,在本地文件系统和 NFS 上语义可靠。NFS 的 `flock` 通过 NLM 协议实现,对同一 NFS server 上的多个客户端提供互斥语义。正常路径下不会有并发写入(管理面四层串行化已保证),flock 仅作为防御性保护防止异常重入。 + +### 5.5 O_DIRECT 内存对齐 + +O_DIRECT 要求用户态 buffer 地址和长度对齐到页大小(4KB)。`ctypes.create_string_buffer` 分配的内存**不保证**特定对齐。 + +**解决方案**:使用 `posix_memalign` + `ctypes` 封装为 `AlignedBuffer` 类。 + +--- + +## 6. AlignedBuffer 参考实现 + +> **Fallback 策略**:若 `posix_memalign` 不可用(极端环境),可 fallback 到 `mmap` + `MAP_ANONYMOUS` 分配页对齐内存。但 `posix_memalign` 在所有主流 Linux 发行版上均可用(glibc 2.0+),fallback 仅作防御性预留。 + +```python +import ctypes +import errno +import os + +_libc = ctypes.CDLL('libc.so.6', use_errno=True) + +class AlignedBuffer(object): + """Page-aligned buffer for O_DIRECT I/O. Use as context manager.""" + + def __init__(self, size, alignment=4096): + self._alignment = alignment + self._size = ((size + alignment - 1) // alignment) * alignment + self._ptr = ctypes.c_void_p() + ret = _libc.posix_memalign( + ctypes.byref(self._ptr), alignment, self._size) + if ret != 0: + raise OSError(ret, "posix_memalign failed") + # Zero-fill + ctypes.memset(self._ptr, 0, self._size) + + def fill(self, data, offset=0): + """Copy data into buffer at given offset.""" + ctypes.memmove(self._ptr.value + offset, data, len(data)) + + def read(self, length, offset=0): + """Read bytes from buffer.""" + return ctypes.string_at(self._ptr.value + offset, length) + + def pwrite(self, fd, file_offset): + """Write buffer contents to fd at file_offset using pwrite.""" + written = 0 + while written < self._size: + ret = _libc.pwrite( + fd, + self._ptr.value + written, + self._size - written, + ctypes.c_longlong(file_offset + written) + ) + if ret < 0: + err = ctypes.get_errno() + if err == errno.EINTR: + continue + raise OSError(err, "pwrite failed: " + os.strerror(err)) + if ret == 0: + raise IOError("pwrite returned 0 (disk full?)") + written += ret + + def pread(self, fd, file_offset): + """Read from fd at file_offset into buffer using pread.""" + read_bytes = 0 + while read_bytes < self._size: + ret = _libc.pread( + fd, + self._ptr.value + read_bytes, + self._size - read_bytes, + ctypes.c_longlong(file_offset + read_bytes) + ) + if ret < 0: + err = ctypes.get_errno() + if err == errno.EINTR: + continue + raise OSError(err, "pread failed: " + os.strerror(err)) + if ret == 0: + raise IOError("pread returned 0 (unexpected EOF)") + read_bytes += ret + + def close(self): + if self._ptr.value: + _libc.free(self._ptr) + self._ptr = ctypes.c_void_p() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def __del__(self): + self.close() +``` + +**使用示例**: + +```python +# 写入 Header (4KB) +with AlignedBuffer(4096) as buf: + buf.fill(header_bytes) + buf.pwrite(fd, 0) + +# 读取 Slot (up to 1MB optimistic) +with AlignedBuffer(1 * 1024 * 1024) as buf: + buf.pread(fd, slot_offset) + data = buf.read(expected_size) + +# 写入 Slot — 构造函数自动将 size 向上对齐到 alignment(4096) +# 注意:pwrite 始终写入 self._size(对齐后)字节,创建时应精确传入所需大小 +slot_total = SLOT_HEADER_SIZE + len(payload) + CHECKSUM_SIZE # 36 + N + 32 +with AlignedBuffer(slot_total) as buf: # e.g. 36+1000+32=1068 → 自动对齐为 4096 + buf.fill(slot_bytes) + buf.pwrite(fd, slot_offset) +``` + +## 7. 约束与不変量 + +1. **I/O 对齐不変量**:所有 Header/Slot 读写必须通过 4KB 对齐缓冲区执行,且使用 `O_DIRECT | O_SYNC`。 +2. **I/O 完整性不変量**:`pwrite/pread` 必须循环至 `self._size` 全部写完/读完;任何 short write/read 都不能被当作成功返回。 +3. **中断处理不変量**:遇到 `EINTR` 必须重试,不允许直接上抛导致部分数据路径中断。 +4. **扫描时延上限不変量**:Layer 4 brute-force 扫描总时长上限 30 秒,超时统一返回 `CORRUPTED`。 +5. **诊断可观测不変量**:brute-force 扫描开始时必须记录 `LV size` 和 `max_steps`,超时必须记录 ERROR。 diff --git "a/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" "b/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..383652a4cfd --- /dev/null +++ "b/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" @@ -0,0 +1,521 @@ +# VM 元数据 — API 设计 + +## 目录 + +1. [API 总览](#1-api-总览) +2. [扫描虚拟机元数据](#2-扫描虚拟机元数据) +3. [读取虚拟机元数据](#3-读取虚拟机元数据) +4. [注册虚拟机](#4-注册虚拟机) +5. [检查虚拟机元数据一致性](#5-检查虚拟机元数据一致性) +6. [运维辅助 API](#6-运维辅助-api) +7. [内部消息](#7-内部消息) +8. [公共参数](#8-公共参数) +9. [统一错误码](#9-统一错误码) + +--- + +## 1. API 总览 + +| # | API | 方向 | 权限 | 说明 | +|---|-----|------|------|------| +| 1 | `APIScanVmInstanceMetadataMsg` | 外部 | admin | 扫描主存储,返回有元数据的 VM 列表 | +| 2 | `APIReadVmInstanceMetadataMsg` | 外部 | admin | 读取指定 VM 的元数据 JSON | +| 3 | `APIRegisterVmInstanceFromMetadataMsg` | 外部 | admin | 从元数据注册 VM | +| 4 | `APICheckVmInstanceMetadataConsistencyMsg` | 外部 | admin | 检查 DB 与存储上元数据的一致性 | +| 5 | `APIUpdateVmMetadataMsg` | 外部 | admin | 手动触发指定 VM 的元数据全量刷写 | +| 6 | `APIPreCheckVmMetadataRegistrationMsg` | 外部 | admin | 注册前预检查 | +| 7 | `APICleanupVmInstanceMetadataMsg` | 外部 | admin | 批量清理指定范围的元数据文件/LV(仅 `enabled=false` 时可用) | +| 8 | `UpdateVmInstanceMetadataMsg` | 内部 | — | Poller/triggerFlush 发送给 VmInstanceBase | +| 9 | `UpdateVmInstanceMetadataOnPrimaryStorageMsg` | 内部 | — | 发送给主存储 handler | +| 10 | `UpdateVmInstanceMetadataOnHypervisorMsg` | 内部 | — | 发送给 Host Agent | + +--- + +## 2. 扫描虚拟机元数据 + +### 2.1 APIScanVmInstanceMetadataMsg + +扫描指定主存储上的元数据,返回有元数据文件/LV 的 VM 列表及摘要信息。 + +> **命名理由**:使用 `Scan` 而非 `Get`,因为该 API 不是从 DB 查询,而是触发 Agent 扫描存储,是一次性 I/O 操作。`Scan` 语义更准确,避免与标准 `APIGet*` 查询模式混淆。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| primaryStorageUuids | List\ | 否 | 指定主存储 UUID 列表;为空则扫描所有已连接 PS | +| vmUuids | List\ | 否 | 仅扫描指定 VM 的元数据;为空则扫描全部 | + +**响应 — APIScanVmInstanceMetadataReply** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| metadataList | List\ | 扫描结果列表 | + +**VmMetadataScanResult** + +| 字段 | 类型 | 说明 | +|------|------|------| +| vmUuid | String | VM UUID | +| vmName | String | VM 名称(来自 sblk Header 摘要 / JSON 文件内容) | +| vmCategory | String | VM 类别(REGULAR / TEMPLATE / TEMPLATE_CACHE) | +| primaryStorageUuid | String | 元数据所在主存储 UUID | +| primaryStorageType | String | 主存储类型(SharedBlock / LocalStorage / NFS) | +| schemaVersion | String | 元数据 schema 版本 | +| lastUpdateTime | Long | 最后更新时间戳(epoch ms) | +| metadataPath | String | 元数据路径(sblk LV path / JSON file path) | +| sizeBytes | Long | 元数据占用空间(字节) | + +### 2.2 实现说明 + +- sblk:调用 Agent 扫描 VG 中所有 `*_vmmeta` LV,读取 Header 提取摘要信息(见 Part 4e §1) +- local/NFS:扫描 `{mountPath}/.zstack-vm-metadata/` 目录下 JSON 文件 +- 扫描结果不含元数据内容,仅含摘要(轻量级) +- 对应的 Java 文件为 `APIScanVmInstanceMetadataMsg.java` / `APIScanVmInstanceMetadataReply.java`,位于 `header/storage/primary/` + +> v2+ 规划(Q5-1):Scan API 将补充分页参数 `start`/`limit`(或 `offset`/`limit`),避免大规模环境一次性返回过大结果集。 + +--- + +## 3. 读取虚拟机元数据 + +### 3.1 APIReadVmInstanceMetadataMsg + +读取指定 VM 的完整元数据 JSON 内容。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| vmUuid | String | 是 | 要读取的 VM UUID | +| primaryStorageUuid | String | 是 | 元数据所在主存储 UUID | + +> **空 payload 处理**:若读取到的 payload 为空 JSON `{}`(初始化后尚未写入完整数据),仍返回 `readStatus=OK` + `metadataContent="{}"`。调用方检查 payload 内容有无实质字段决定是否可注册。 +> +> **`__readStatus` 嵌入**:Read API 在返回 `metadataContent` 时,将当前 `readStatus` 值以 `"__readStatus": ""` 字段嵌入 JSON 根级别。此字段供 Register API 入口校验数据可用性(见 [Part 3 §3.3-1](vm-metadata-03-注册与运维.md#33-完整注册步骤))。手动构造的 metadataContent 若不含此字段,Register 视为 OK 继续。 + +**响应 — APIReadVmInstanceMetadataReply** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| metadataContent | String | 完整元数据 JSON 字符串 | +| schemaVersion | String | 元数据 schema 版本 | +| readStatus | String | OK / NEED_REPAIR / RECOVERED / DEGRADED / STORAGE_CHANGE_INCOMPLETE / CORRUPTED | +| repairAction | String | 可为 null。NEED_REPAIR/RECOVERED 时提示的修复动作(如 "complete_phase3" / "rebuild_header" / "full_refresh") | +| warnings | List\ | 读取过程中的非致命警告 | + +### 3.2 readStatus 说明 + +| 状态 | 含义 | payload | is_usable | 后续操作 | +|------|------|---------|-----------|----------| +| OK | 正常读取,Checksum 校验通过 | (Y) 有效 | (Y) | — | +| NEED_REPAIR | Slot 可读但 Header 需修复(sblk,PendingOp 残留) | (Y) 有效 | (Y) | 管理平面发送 `RepairMetadataMsg` | +| RECOVERED | Header 损坏但通过 Slot 自描述恢复成功 | (Y) 有效 | (Y) | 管理平面发送 `RepairMetadataMsg` 重建 Header | +| STORAGE_CHANGE_INCOMPLETE | 存储拓扑已变更但元数据未更新(PendingOp=2 且 Phase 2 未完成) | (!) stale | (N) | **禁止注册**,必须 `markDirty` 全量重写 | +| DEGRADED | 单 Slot 损坏,通过另一 Slot 降级读取成功 | (!) 有效(非最新) | (Y) | 允许注册(如灾备场景)+ 必须告警 + 触发修复 | +| CORRUPTED | A/B 双 Slot 均损坏(sblk)或文件内容无效 | (N) | (N) | `markDirty` 全量重写 | + +> sblk 读取与恢复的完整流程见 [Part 4d](vm-metadata-04d-sblk读取与恢复.md)。 +> +> v2+ 规划(Q5-2):Read API 增加 streaming/分块返回模式,降低超大 payload 场景下单次响应体压力。 + +--- + +## 4. 注册虚拟机 + +### 4.1 APIRegisterVmInstanceFromMetadataMsg + +从元数据注册虚拟机。详细注册流程见 [Part 3](vm-metadata-03-注册与运维.md)。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| metadataContent | String | 是 | 完整元数据 JSON(通常来自 `APIReadVmInstanceMetadataMsg` 的响应)。大小限制:超过 30MB 拒绝 | +| targetPrimaryStorageUuid | String | 是 | 目标主存储 UUID | +| zoneUuid | String | 是 | 目标 Zone UUID | +| clusterUuid | String | 是 | 目标 Cluster UUID | +| forceVersionMismatch | Boolean | 否 | 默认 false。设为 true 时允许 schemaVersion 不匹配的强制注册 | + +**响应 — APIRegisterVmInstanceFromMetadataEvent** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| inventory | VmInstanceInventory | 注册成功的 VM Inventory | +| warnings | List\ | 注册过程中的非致命警告(如 imageUuid 不存在、diskOfferingUuid 已清空、模板 VM 降级为普通 VM 等) | + +> **LongJob**:注册操作通过 `LongJob` 框架异步执行,超时时间默认 30 分钟。原因:注册涉及大量 DB 写入 + Agent 调用(快照链变基),耗时可能较长。LongJob 提供进度查询、超时保护和 API 线程释放。 +> +> **输入校验**:`metadataContent` 大小超过 30MB 立即拒绝(与 Part 2b §10 的 payload 大小保护一致)。校验在 API 入口层执行,在 JSON 解析之前。 + +### 4.2 状态转换 + +``` +(new) → Registering → Stopped + │ + └── 失败 → 回滚删除所有 VO +``` + +注册成功后 VM 处于 `Stopped` 状态。用户需要先添加网卡(`AttachVmNicToVm`)再启动。 + +### 4.3 注册后首次启动 + +VM 首次从 Stopped 转为 Running 时,删除 `vm.metadata.registered.not.started` ResourceConfig,立即触发 `markDirty`。此后元数据正常跟踪(见 [Part 3 §3.2](vm-metadata-03-注册与运维.md#32-注册-vm-未首次启动-resourceconfig))。 + +### 4.4 v2+ 规划:批量注册 API + +> 当前注册 N 个 VM 需执行 4N 次 API 调用(Scan + Read + PreCheck + Register × N),大规模灾备恢复场景(100+ VM)效率较低。 +> +> v2+ 计划引入 `APIBatchRegisterVmInstanceFromMetadataMsg`: +> - **输入**:`vmUuids`(List\)+ `primaryStorageUuid` + `zoneUuid` + `clusterUuid`。内部自动执行 Read + PreCheck + Register。 +> - **并行策略**:按 `vm.metadata.global.maxConcurrent` 控制并行度,分批注册。 +> - **部分成功**:返回 `List`,每个 VM 独立成功/失败,不因单 VM 失败中止整批。 +> - **进度查询**:通过 LongJob 框架提供进度百分比(已完成 / 总数)。 + +--- + +## 5. 检查虚拟机元数据一致性 + +### 5.1 APICheckVmInstanceMetadataConsistencyMsg + +从 DB 构建当前元数据 → 从存储读取已持久化元数据 → 结构化比较。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| vmUuids | List\ | 否 | 指定 VM 列表;为空则检查所有已启用元数据的 VM | +| primaryStorageUuid | String | 否 | 限定主存储范围 | +| autoRepair | Boolean | 否 | 默认 `false`。`true` 时对可修复不一致项自动执行 `markDirty(vmUuid)`(v1.1 新增) | + +**响应 — APICheckVmInstanceMetadataConsistencyReply** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| results | List\ | 每个 VM 的检查结果 | + +**ConsistencyCheckResult** + +| 字段 | 类型 | 说明 | +|------|------|------| +| vmUuid | String | VM UUID | +| consistent | Boolean | 是否一致 | +| diffs | List\ | 不一致的项目列表 | +| action | String | 自动修复操作(NONE / MARK_DIRTY) | + +### 5.2 比较排除字段 + +以下字段在比较时忽略(属于运行时变化字段): + +- `lastOpDate` — 时间戳字段 +- `id` — 自增 ID(SystemTag、ResourceConfig) +- `managementNodeUuid` — 运行时绑定 MN + +> **Q37 — 排除字段完整清单**:除上述 3 项外,以下字段也需排除: +> - `accountUuid` — 注册场景中会被替换为目标环境 accountUuid,不应参与比对 +> - `createDate` — 新创建 VO 的 createDate 与元数据中记录的不同(每次 persist 时由 DB 生成) +> - `VmInstanceVO.hostUuid` — 运行时动态绑定,VM Stopped 时为 null +> - `VmInstanceVO.lastHostUuid` — 运行时动态绑定 +> - `VmInstanceVO.state` — 运行时状态,不属于结构化配置 +> - `VolumeVO.actualSize` — Agent 端物理大小,不参与元数据一致性判定 +> - `VolumeVO.status` — 运行时状态(Ready/NotInstantiated) +> +> **比对逻辑**:先按 `VmInstanceVO.uuid` 匹配 VM 主记录,再按各子 VO 的 `uuid` 逐项匹配 Volume/Snapshot/SystemTag/ResourceConfig。匹配成功后逐字段比对(排除上述字段)。排除字段列表允许通过 `ConsistencyCheckExcludedFields` 静态常量扩展,新增字段时添加注释说明排除原因。 + +### 5.3 自动修复 + +发现不一致时,自动调用 `markDirty(vmUuid)` 触发全量重写。这是内部消息丢失 `markDirty()` 的批量补救手段(见 [Part 2b §12.4 D1 补充说明](vm-metadata-02b-高可用与运维.md#d1-补充说明--内部消息-handler-遗漏-markdirty-的补救))。 + +> 行为约束(Q5-3):仅当 `autoRepair=true` 时执行自动修复;默认 `false` 只返回检查结果与建议动作,避免检查 API 带来隐式写入副作用。 + +**自动修复边界表**: + +| 场景 | DB 状态 | 存储元数据状态 | 修复动作 | 说明 | +|------|---------|--------------|----------|------| +| DB 比存储新 | 有字段差异 | 旧版本 | MARK_DIRTY | 正常情况,刷写延迟或 markDirty 遗漏 | +| DB 缺少 UUID | VM 存在 | 存储上有元数据但 UUID 未在 DB 中 | MANUAL_CHECK | 可能是孤儿元数据,需人工确认 | +| 存储元数据损坏 | VM 存在 | CORRUPTED / 无法解析 | MARK_DIRTY | 全量重建 | +| 存储不可达 | VM 存在 | 无法读取 | SKIP + WARN | 不执行修复,记录告警 | + +--- + +## 6. 运维辅助 API + +### 6.1 手动触发元数据更新 + +#### APIUpdateVmMetadataMsg + +指定 vmUuid,手动触发一次全量元数据更新。适用于运维人员发现元数据滞后时的即时修复。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| vmUuid | String | 是 | 目标 VM UUID | + +**响应 — APIUpdateVmMetadataEvent** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | + +**实现**:直接调用 `markDirty(vmUuid, true)` 标脏为 STORAGE 级别(全量),triggerFlush 立即处理。 + +> 并发说明(Q5-4):`APIUpdateVmMetadataMsg` 的同 VM 并发更新由 `ChainTask "update-vm-{vmUuid}-metadata"` 串行化保证,无需额外 API 级锁。 + +### 6.2 注册预检查 + +#### APIPreCheckVmMetadataRegistrationMsg + +在正式注册前执行预检查,返回所有检查项的通过/失败状态,帮助用户提前发现问题。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| metadataContent | String | 是 | 完整元数据 JSON | +| targetPrimaryStorageUuid | String | 是 | 目标主存储 UUID | +| zoneUuid | String | 否 | 目标 Zone UUID。若提供,额外校验 Zone 存在性及与 PS 的归属关系 | +| clusterUuid | String | 否 | 目标 Cluster UUID。若提供,额外校验 Cluster 存在性、与 PS 的连接性、及 Zone/Cluster 归属一致性 | +| forceVersionMismatch | Boolean | 否 | 默认 false。设为 true 时 `SCHEMA_VERSION_MATCH` 检查项不阻塞 | + +**响应 — APIPreCheckVmMetadataRegistrationReply** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| checkResults | List\ | 各检查项结果 | + +**PreCheckItem** + +| 字段 | 类型 | 说明 | +|------|------|------| +| name | String | 检查项名称 | +| passed | Boolean | 是否通过 | +| message | String | 检查详情 / 失败原因 | + +### 6.3 清理虚拟机元数据 + +#### APICleanupVmInstanceMetadataMsg + +批量清理指定范围的虚拟机元数据文件/LV 及关联 DB 记录。仅在 `vm.metadata.enabled=false` 时允许执行。 + +> **使用场景**:运维在关闭元数据功能后(`true → false`),按需回收存储空间。系统不自动清理,避免误操作丢失容灾数据。 + +**前置约束**:`vm.metadata.enabled` 必须为 `false`,否则返回错误 `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`。 + +**请求参数** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| primaryStorageUuids | List\ | 否 | 指定主存储范围。为空则清理所有 PS | +| vmUuids | List\ | 否 | 指定 VM 范围。为空则清理所有 VM | + +> 两个参数均为空时,清理**全部已存在元数据的 VM**。两个参数同时提供时取交集。 + +**响应 — APICleanupVmInstanceMetadataEvent** + +| 字段 | 类型 | 说明 | +|------|------|------| +| success | Boolean | — | +| totalCleaned | Integer | 成功清理的 VM 数量 | +| totalFailed | Integer | 清理失败的 VM 数量 | +| failedVmUuids | List\ | 清理失败的 VM UUID 列表(便于重试) | + +**实现流程**: + +1. 前置检查:`vm.metadata.enabled == false` +2. 根据参数确定清理范围(查 `VmMetadataPathFingerprintVO` 获取有元数据的 VM 列表) +3. 分批执行(keyset 分页,批次大小复用 `vm.metadata.upgrade.refreshBatchSize`): + - 对每个 VM 调用 `metadataStorageHandler.deleteMetadata(psUuid, vmUuid)` + - 删除 `VmMetadataPathFingerprintVO` 记录 + - 删除残留 `VmMetadataDirtyVO` 记录(`INSERT IGNORE` 插入后未消费的行) +4. 汇总结果,部分失败不中止(best-effort),返回失败列表供运维重试 + +**幂等性**:`deleteMetadata` 遵循 C-01C-9 约束(删除不存在的元数据视为成功),重复调用不报错。 + +**并发控制**:使用全局 ChainTask `"cleanup-vm-metadata-global"`(syncLevel=5)限流,避免对存储造成批量删除压力。 + +### 6.4 预检查项清单 + +| 检查项 | 说明 | 失败级别 | +|--------|------|----------| +| `FORMAT_VALID` | 元数据 JSON 格式、Base64 编码完整性 | BLOCK | +| `SCHEMA_VERSION_MATCH` | `schemaVersion == dbf.getDbVersion()`(精确匹配) | BLOCK(除非 `forceVersionMismatch=true`) | +| `VM_CATEGORY_CHECK` | vmCategory 不是 TEMPLATE_CACHE | BLOCK | +| `UUID_CONFLICT` | VM/Volume/Snapshot 等 UUID 无冲突 | BLOCK | +| `PS_REACHABLE` | 目标主存储可达且状态正常 | BLOCK | +| `PS_TYPE_SUPPORTED` | 主存储类型支持元数据(sblk/local/NFS) | BLOCK | +| `CROSS_STORAGE_CHECK` | 所有磁盘属于同一主存储 | BLOCK | +| `INSTALL_PATH_EXIST` | 替换后路径在存储上存在。Root Volume 缺失为 BLOCK,Data Volume 缺失为 WARN | Root=BLOCK / Data=WARN | +| `READ_STATUS_USABLE` | 元数据 `__readStatus` 不为 CORRUPTED 或 STORAGE_CHANGE_INCOMPLETE | BLOCK | +| `CDROM_DETECTED` | 检测到 VM 挂载了 CDROM / ISO,注册后可能不可用 | WARN | + +> **schemaVersion 校验逻辑**:使用精确匹配 `==` 比较数据库版本。不支持低版本数据库注册高版本元数据,也不支持高版本数据库注册低版本元数据(除非 `forceVersionMismatch=true`)。参见 [Part 1a §6.2](vm-metadata-01a-数据模型与序列化.md#62-注册时校验规则)。 + +`INSTALL_PATH_EXIST` 检查实现示例(Q5-6): + +```java +if (volume.isRootVolume() && !pathExists(volume.getInstallPath())) { + result.add(PreCheckItem.block(INSTALL_PATH_EXIST, ...)); +} else if (!pathExists(volume.getInstallPath())) { + result.add(PreCheckItem.warn(INSTALL_PATH_EXIST, ...)); +} +``` + +> `CDROM_DETECTED` 处理说明(Q5-7):CDROM/ISO 挂载信息不在 VM 元数据范围内,注册后如业务需要须手动重新挂载。 + +--- + +## 7. 内部消息 + +### 7.1 UpdateVmInstanceMetadataMsg + +由 Poller/triggerFlush 发送给 `VmInstanceBase`,触发构建元数据并写入主存储。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| uuid | String | VM UUID | +| storageStructureChange | Boolean | 是否涉及存储拓扑变更(OP type 标记) | + +路由:`makeLocalServiceId(msg, VmInstanceConstant.SERVICE_ID)` + +超时:`setTimeout(5min)` — 大 payload O_DIRECT 写入 + 可能的 lvextend + 构建耗时(与 Part 2 §5.1 一致) + +### 7.2 UpdateVmInstanceMetadataOnPrimaryStorageMsg + +由 VmInstanceBase 发送给主存储 handler。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| vmUuid | String | VM UUID | +| payload | String | 序列化后的元数据 JSON | +| storageStructureChange | Boolean | OP type | + +路由:`makeLocalServiceId` + +### 7.3 UpdateVmInstanceMetadataOnHypervisorMsg + +由主存储 handler 发送给 Host Agent。 + +> **Agent 通信安全**:HTTP 请求携带 `agentToken`(通过 `X-ZStack-Agent-Token` header 传递),Agent 端校验 token 一致性。这与 ZStack 其他 Agent 通信一致,无额外认证机制。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| hostUuid | String | 目标主机 UUID | +| vmUuid | String | VM UUID | +| payload | String | 元数据 JSON | +| installPath | String | 元数据存储路径 | +| storageStructureChange | Boolean | OP type | + +路由:`makeTargetServiceIdByResourceUuid(hostUuid)` — hash 环路由到 host-owner MN + +超时:`setTimeout(2min)` + +### 7.4 消息调用链 + +``` +VmMetadataUpdateInterceptor / Poller + → markDirty + triggerFlushForVm + → ChainTask "update-vm-metadata-global" + → ChainTask "update-vm-{vmUuid}-metadata" + → bus.send(UpdateVmInstanceMetadataMsg) + → VmInstanceBase: build payload + → bus.send(UpdateVmInstanceMetadataOnPrimaryStorageMsg) + → PS handler: ChainTask "update-metadata-on-ps-{psUuid}" + → bus.send(UpdateVmInstanceMetadataOnHypervisorMsg) + → HostBase → HTTP call to Agent +``` + +完整消息链描述见 [Part 2 §5](vm-metadata-02-脏标记与Poller.md#5-消息调用链)。 + +### 7.5 RepairMetadataMsg + +由管理平面发送给主存储 handler,用于修复 sblk Header(包括完成未完成的 Phase 3、清除 PendingOp、重建 Header)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| vmUuid | String | VM UUID | +| primaryStorageUuid | String | 元数据所在主存储 UUID | +| repairAction | String | 修复动作:`complete_phase3` / `clear_pending_op` / `rebuild_header` / `full_refresh` | + +路由:`makeLocalServiceId` → 主存储 handler → Agent HTTP 调用 + +> `full_refresh` 等价于 `markDirty(vmUuid, true)`,但通过显式消息而非 Poller 间接触发,便于日志追踪。 + +### 7.6 BatchCheckMetadataStatusMsg + +由管理平面发送给主存储 handler,批量检查多个 VM 的元数据 Header 状态,用于健康巡检。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| primaryStorageUuid | String | 目标主存储 UUID | +| vmUuids | List\ | 要检查的 VM UUID 列表 | + +**响应 — BatchCheckMetadataStatusReply** + +| 字段 | 类型 | 说明 | +|------|------|------| +| results | Map\ | key=vmUuid, value=状态结果 | + +**MetadataStatusResult** + +| 字段 | 类型 | 说明 | +|------|------|------| +| readStatus | String | OK / NEED_REPAIR / RECOVERED / DEGRADED / STORAGE_CHANGE_INCOMPLETE / CORRUPTED | +| repairAction | String | 可为 null | +| lastUpdateTime | Long | 最后更新时间戳 | +| pendingOp | Integer | 当前 PendingOp 值(0/1/2) | + +路由:`makeLocalServiceId` → 主存储 handler → Agent HTTP 调用(批量读 Header,不读 Slot) + +--- + +## 8. 公共参数 + +### 8.1 GlobalConfig + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `vm.metadata.enabled` | Boolean | false | 元数据功能总开关 | +| `vm.metadata.dirty.pollIntervalSec` | Long | 5 | Poller 轮询间隔(秒) | +| `vm.metadata.dirty.batchSize` | Integer | 50 | 每轮 Poller 最多认领行数 | +| `vm.metadata.maxRetry` | Integer | 5 | 最大重试次数 | +| `vm.metadata.ps.maxConcurrent` | Integer | 5 | 同一 MN 同一 PS 最大并发写入 | +| `vm.metadata.global.maxConcurrent` | Integer | 10 | 同一 MN 最大并发 VM 更新数 | +| `vm.metadata.pathCheck.intervalSec` | Long | 300 | 路径指纹巡检间隔(秒) | + +> 完整 GlobalConfig 配置说明见 [Part 2b §13](vm-metadata-02b-高可用与运维.md#13-globalconfig-配置项汇总)(权威来源)。本表仅为快速参考。 + +### 8.2 权限约束 + +所有 API 仅限 **admin** 操作。注册 VM 场景为灾难恢复,不面向普通用户。 + +--- + +## 9. 统一错误码 + +> **权威来源**:所有与 VM 元数据相关的错误码在此统一定义。其他文档应引用本节。 + +| 错误码 | 适用 API | 说明 | +|--------|---------|------| +| `METADATA_INVALID_FORMAT` | Read / Register / PreCheck | 元数据 JSON 格式错误、Base64 解码失败或校验器不通过 | +| `METADATA_SCHEMA_VERSION_MISMATCH` | Register / PreCheck | `schemaVersion != dbf.getDbVersion()`,且未设置 `forceVersionMismatch=true` | +| `METADATA_UUID_CONFLICT` | Register / PreCheck | VM、Volume、Snapshot 等 UUID 与已有资源冲突 | +| `METADATA_STORAGE_NOT_SUPPORTED` | Register / PreCheck / Scan | 主存储类型不支持元数据功能(如 Ceph、ZBS、vhost) | +| `METADATA_CROSS_STORAGE_FORBIDDEN` | Register / PreCheck | 元数据中的磁盘分布在多个主存储上 | +| `METADATA_INSTALL_PATH_NOT_FOUND` | Register | 替换后的 installPath 在目标存储上不存在 | +| `METADATA_CACHE_VM_NOT_REGISTERABLE` | Register / PreCheck | vmCategory = TEMPLATE_CACHE,缓存 VM 拒绝注册 | +| `METADATA_VM_REGISTERING` | Register | 目标 VM 正在被另一个注册操作处理中 | +| `METADATA_READ_CORRUPTED` | Read | A/B 双 Slot 均损坏(sblk)或文件不可读 | +| `METADATA_PAYLOAD_TOO_LARGE` | Update(内部) | Payload 超过 30MB 上限 | +| `METADATA_PS_UNREACHABLE` | PreCheck / Register / Update | 目标主存储不可达或状态异常 | +| `METADATA_FEATURE_DISABLED` | All | `vm.metadata.enabled = false` 时调用 API | + +### 9.1 错误码格式 + +所有错误码使用 `SysErrors.METADATA_` 前缀,在 `VmMetadataErrors` 枚举中统一定义。API Reply/Event 中通过标准 `ErrorCode` 结构返回。 diff --git "a/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" "b/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" new file mode 100644 index 00000000000..25712a15d44 --- /dev/null +++ "b/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" @@ -0,0 +1,278 @@ +# VM 元数据 — 单元测试计划 + +> 本文档为 VM 元数据功能的单元测试计划。单元测试聚焦**纯 Java 逻辑**,不依赖 DB/Agent/存储,通过 mock 隔离外部依赖。 +> 集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 + +## 目录 + +1. [元数据序列化 Round-Trip](#1-元数据序列化-round-trip) +2. [DTO 字段完整性](#2-dto-字段完整性) +3. [路径指纹与漂移检测](#3-路径指纹与漂移检测) +4. [markDirty 逻辑](#4-markdirty-逻辑) +5. [MetadataImpact 注解覆盖率](#5-metadataimpact-注解覆盖率) +6. [VM UUID Resolver 链](#6-vm-uuid-resolver-链) +7. [Payload 容量计算](#7-payload-容量计算) +8. [sblk 二进制布局编解码](#8-sblk-二进制布局编解码) +9. [注册字段映射](#9-注册字段映射) +10. [installPath 前缀替换](#10-installpath-前缀替换) + +--- + +## 1. 元数据序列化 Round-Trip + +**覆盖约束**:Part 1a §2–§3 + +### 1.1 基础 Round-Trip + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-SER-01 | 最小 VM(单根盘、无快照、无数据盘) | 构造最小 `VmInstanceMetadataDTO` | Gson 序列化 → 反序列化 → 与原对象 `equals` | +| UT-SER-02 | 完整 VM(根盘 + 3 数据盘 + 快照链 + NIC + SystemTag + ResourceConfig) | 构造满字段 DTO | Round-Trip 后所有字段一致 | +| UT-SER-03 | 含 null 字段的 VM(imageUuid=null, instanceOfferingUuid=null) | DTO 部分字段为 null | Gson 序列化跳过 null 字段(`serializeNulls=false`),反序列化后 null 字段仍为 null | +| UT-SER-04 | 空快照列表 | `snapshots = Collections.emptyList()` | 序列化为 `"snapshots":[]`,反序列化后 `.size()==0` 且非 null | +| UT-SER-05 | 深度快照链(256 层嵌套 parentUuid) | 构造 depth=256 的链式快照 | Round-Trip 后 parentUuid 链完整保留 | + +### 1.2 编码管线 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-SER-10 | SystemTag Base64 编码 Round-Trip | `List` → JSON → Base64 → DTO.systemTags (String) | 解码后还原为等价 `List` | +| UT-SER-11 | ResourceConfig Base64 编码 Round-Trip | 同上 | 解码后还原 | +| UT-SER-12 | 空 SystemTag 列表 | `systemTags = []` → Base64 | 编码非空字符串,解码还原为空列表 | +| UT-SER-13 | 含特殊字符的 tag(中文、emoji、`=` 分隔符) | `tag::key::中文值(fire)` | Round-Trip 后完整保留 | + +### 1.3 JSON 确定性 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-SER-20 | 同一 DTO 多次序列化字节一致 | 同一对象序列化 10 次 | 10 次 `byte[]` 完全相同 | +| UT-SER-21 | 字段声明顺序稳定性 | 检查 Gson 输出中 `uuid` 在 `name` 前(按声明顺序) | JSON key 顺序与 Java 字段声明顺序一致 | +| UT-SER-22 | `@SerializedName` 注解生效 | DTO 中带 `@SerializedName("vm_uuid")` 的字段 | JSON key 为 `vm_uuid` 而非 Java 字段名 | + +### 1.4 版本兼容 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-SER-30 | 反序列化缺失字段(旧版本数据) | 不含新版字段 `vmCategory` 的 JSON | 反序列化成功,`vmCategory==null` | +| UT-SER-31 | 反序列化多余字段(新版本数据) | JSON 含当前 DTO 无对应的 `futureField` | Gson 默认忽略未知字段,不报错 | +| UT-SER-32 | schemaVersion 精确匹配检查 | `expected=3, actual=2` | `isVersionMatch()` 返回 false | + +--- + +## 2. DTO 字段完整性 + +**覆盖约束**:Part 1a §1, §4 + +### 2.1 快照树构建 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-DTO-01 | 空快照列表构建树 | `List = []` | 返回空树列表 | +| UT-DTO-02 | 单棵树、线性链(A→B→C) | 3 个 VO,parentUuid 链式指向 | 树结构正确:root=A, A.children=[B], B.children=[C] | +| UT-DTO-03 | 多棵独立树 | 2 棵树各 3 个节点,volumeSnapshotTreeUuid 不同 | 返回 2 棵独立树 | +| UT-DTO-04 | 分叉树(A→B, A→C) | 3 个 VO,B 和 C 的 parentUuid 都指向 A | A.children 包含 B 和 C | +| UT-DTO-05 | 共享磁盘快照排除 | 快照列表含 `VolumeVO.isShareable=true` 的卷快照 | 构建时跳过该卷的快照 | + +### 2.2 VolumeSnapshotReferenceVO 查询范围 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-DTO-10 | 按 referenceVolumeUuid 查询(非 volumeUuid) | mock DB 返回:ref.referenceVolumeUuid 匹配当前 VM 卷 | 仅返回当前 VM 的引用记录,不含父模板的引用 | +| UT-DTO-11 | VM 无引用记录 | `referenceVolumeUuid` 无匹配 | 返回空列表 | + +### 2.3 SystemTag/ResourceConfig 白名单过滤 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-DTO-20 | 白名单内 tag 保留 | `bootMode::UEFI` | 保留在构建结果中 | +| UT-DTO-21 | 白名单外 tag 过滤 | `ephemeral::true`(假设不在白名单) | 不在构建结果中 | +| UT-DTO-22 | ResourceConfig 按类型分组 | VM 级 + Volume 级 config | 分别归入 `vmConfigs` 和 `volumeConfigs` | + +--- + +## 3. 路径指纹与漂移检测 + +**覆盖约束**:Part 2b §8.2 + +### 3.1 路径快照构建 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-FP-01 | 正常路径快照 JSON | 2 个 Volume + 3 个 Snapshot(有序) | JSON 中 volumes/snapshots 按 uuid ASC 排列 | +| UT-FP-02 | 相同拓扑的确定性 | 两次构建(不同对象实例,相同内容) | JSON 字符串 `byte[]` 完全相同 | +| UT-FP-03 | 空快照列表 | 仅有 Volume 无 Snapshot | `"snapshots":[]`,不影响比对 | + +### 3.2 路径漂移检测 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-FP-10 | 无漂移 | recorded == current | 不触发 markDirty | +| UT-FP-11 | Volume installPath 变更 | current 中 vol-aaa 路径不同 | 检测到 drift → 调用 markDirty | +| UT-FP-12 | Snapshot 新增 | current 中多一个 snapshot | 检测到 drift | +| UT-FP-13 | Snapshot 删除 | current 中少一个 snapshot | 检测到 drift | + +--- + +## 4. markDirty 逻辑 + +**覆盖约束**:C-DM-01, C-SC-07, C-FL-08 + +### 4.1 标脏语义 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-MD-01 | 首次 markDirty | vmUuid 不存在 dirty 行 | INSERT IGNORE 创建新行,`dirtyVersion=1` | +| UT-MD-02 | 重复 markDirty | vmUuid 已有 dirty 行 | UPDATE `dirtyVersion+1`,行唯一 | +| UT-MD-03 | storageStructureChange OR 语义 | 先 markDirty(false) 再 markDirty(true) | dirty 行 `storageStructureChange=true`(不会被覆盖回 false) | +| UT-MD-04 | storageStructureChange 反向不降级 | 先 markDirty(true) 再 markDirty(false) | 仍为 `storageStructureChange=true` | +| UT-MD-05 | vm.metadata.enabled=false 时 markDirty | 开关关闭 | markDirty 直接 return,不创建 dirty 行 | +| UT-MD-06 | Destroyed VM 不标脏 | vmState=Destroyed | markDirty 直接 return(C-FL-08 前置过滤) | + +### 4.2 retryCount 与退避 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-MD-10 | 新 markDirty 的 retryCount | 首次创建 | `retryCount=0, nextRetryTime=NULL` | +| UT-MD-11 | markDirty 不重置已有 retryCount | dirty 行 retryCount=3 时再次 markDirty | retryCount 不变(仅递增 dirtyVersion) | +| UT-MD-12 | 指数退避计算 | baseDelay=10s, maxExponent=10, retryCount=3 | `nextRetryTime = now + 10 * 2^3 = 80s` | +| UT-MD-13 | 退避上限 | retryCount=15(超过 maxExponent=10) | `nextRetryTime = now + 10 * 2^10 = 10240s`(封顶) | + +--- + +## 5. MetadataImpact 注解覆盖率 + +**覆盖约束**:C-IM + +### 5.1 CI 扫描验证 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-AN-01 | 所有 APIMessage 子类有 @MetadataImpact | 反射扫描所有 `APIMessage` 子类 | 每个子类都标注了 `@MetadataImpact`(NONE/CONFIG/STORAGE) | +| UT-AN-02 | STORAGE 级 API 不误标为 CONFIG | 检查 APIDeleteVolumeSnapshotMsg 等 | 快照/迁移/删盘类 API 标注为 `Impact.STORAGE` | +| UT-AN-03 | 纯查询 API 标注 NONE | 检查 QueryVmInstanceMsg 等 | `Impact.NONE` | + +### 5.2 内部消息白名单 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-AN-10 | INTERNAL_METADATA_MESSAGES 包含所有 STORAGE 级内部消息 | 反射扫描 + 对比白名单 | 白名单完整 | +| UT-AN-11 | 白名单中每个消息的 handler 调用了 markDirty | 静态分析 / mock 验证 | 所有 handler 成功路径包含 markDirty 调用 | + +--- + +## 6. VM UUID Resolver 链 + +**覆盖约束**:C-RS, Part 1b §3 + +### 6.1 Resolver 选择 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-RS-01 | VmInstanceMessage 直接获取 vmUuid | APIStopVmInstanceMsg(vmUuid="vm-1") | Resolver 返回 `["vm-1"]` | +| UT-RS-02 | VolumeMessage 通过 Volume→VM 查询 | APIDeleteVolumeMsg(volumeUuid="vol-1") + mock vol-1.vmInstanceUuid="vm-1" | 返回 `["vm-1"]` | +| UT-RS-03 | TagMessage 按 resourceType 路由 | APICreateSystemTagMsg(resourceUuid="vol-1", resourceType="VolumeVO") | 返回 vol-1 关联的 VM UUID | +| UT-RS-04 | 无法解析的 API | 自定义 API 无匹配 Resolver | 返回空列表 + WARN 日志 | +| UT-RS-05 | 删除/卸载类 API 使用 pre-capture | APIDetachVolumeMsg | `resolveVmUuids()` 在 `beforeDeliveryMessage` 阶段调用(C-RS) | +| UT-RS-06 | 单个 API 关联多个 VM | 批量 Tag 操作涉及多个 VM | 返回所有关联 VM UUID(去重) | + +--- + +## 7. Payload 容量计算 + +**覆盖约束**:Part 2b §10.0, C-02B-5, C-02B-7 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-CAP-01 | 4MB LV 的 slotCapacity | `lvSize = 4*1024*1024` | `slotCapacity = ((4MB - 4096) / 2 / 4096) * 4096 = 2,093,056` | +| UT-CAP-02 | 64MB LV(上限)的 slotCapacity | `lvSize = 64*1024*1024` | `slotCapacity = 33,550,336` | +| UT-CAP-03 | payload 可用空间 | `slotCapacity - 36 (SlotHeader)` | 4MB LV → 2,093,020 bytes;64MB LV → 33,550,300 bytes | +| UT-CAP-04 | WARN 阈值判定 | payloadSize = 8MB + 1 | 触发 WARN | +| UT-CAP-05 | REJECT 阈值判定 | payloadSize = 30MB + 1 | 触发 ERROR + 拒绝写入 | +| UT-CAP-06 | 常量集中定义验证 | 反射检查 `VmMetadataConstants` 类 | HEADER_SIZE/SLOT_HEADER_SIZE/MAX_LV_SIZE 均为 static final | + +--- + +## 8. sblk 二进制布局编解码 + +**覆盖约束**:Part 4b + +### 8.1 Header 编解码 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-BIN-01 | Header 序列化 Round-Trip | 构造 Header(Magic, ActiveSlot=0, WriteSequence=1, PendingOp=0) | 序列化为 4096 bytes → 反序列化还原 | +| UT-BIN-02 | Magic 校验 | `0x5A534D54` | 读取 Header 校验通过 | +| UT-BIN-03 | Magic 错误 | `0xDEADBEEF` | 读取 Header 抛出 `InvalidHeaderException` | +| UT-BIN-04 | ControlChecksum 校验 | 正常 Header + SHA-256 | checksum 验证通过 | +| UT-BIN-05 | ControlChecksum 篡改 | 修改 Header 一个字节后不更新 checksum | checksum 验证失败 | + +### 8.2 Slot 编解码 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-BIN-10 | Slot Header 序列化 | SlotMagic + SeqNum + Offset + Capacity + PayloadLen | 序列化为 36 bytes | +| UT-BIN-11 | Payload 写入与读取 | 1KB payload → 写入 Slot → 读取 | payload 内容一致 | +| UT-BIN-12 | Payload 恰好填满 Slot | payloadLen == slotCapacity - 36 | 写入成功,无溢出 | +| UT-BIN-13 | Payload 超过 Slot 容量 | payloadLen > slotCapacity - 36 | 抛出 `PayloadTooLargeException` | + +### 8.3 VM 摘要区 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-BIN-20 | 摘要区序列化 | vmUuid(32) + vmName(256) + vmCategory(1) | 写入 [96:928) 区域 | +| UT-BIN-21 | vmName 超过 256 bytes | UTF-8 编码后 > 256 | 截断到 256 bytes 边界(不截断 UTF-8 多字节字符中间) | +| UT-BIN-22 | SummaryChecksum 验证 | 正常摘要区 | SHA-256 通过 | + +### 8.4 WriteSequence 边界 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-BIN-30 | 正常递增 | seq=100 → 写入 → seq=101 | WriteSequence +1 | +| UT-BIN-31 | Long.MAX_VALUE 溢出 | seq=Long.MAX_VALUE | 写入后 seq=Long.MIN_VALUE(Java long 自然溢出),比较逻辑使用 `Long.compareUnsigned` 或差值判断 | + +--- + +## 9. 注册字段映射 + +**覆盖约束**:Part 3 §1 + +### 9.1 VmInstanceVO 映射 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-REG-01 | uuid 保留 | 元数据 vmUuid="vm-aaa" | 注册后 VmInstanceVO.uuid="vm-aaa" | +| UT-REG-02 | hostUuid / lastHostUuid 置 null | 元数据含原始 hostUuid | 注册后两者均为 null | +| UT-REG-03 | state 硬编码 Registering→Stopped | — | 创建时 state=Registering,成功后改为 Stopped | +| UT-REG-04 | imageUuid 不存在时置 null | mock `dbf.findByUuid(imageUuid)` 返回 null | imageUuid=null + warnings 含提示信息 | +| UT-REG-05 | accountUuid 替换为当前调用者 | 元数据 accountUuid="old-admin" | 注册后 accountUuid = 当前 session 的 accountUuid | + +### 9.2 VolumeVO 映射 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-REG-10 | primaryStorageUuid 替换 | 元数据 psUuid="old-ps" + 目标 psUuid="new-ps" | VolumeVO.primaryStorageUuid="new-ps" | +| UT-REG-11 | diskOfferingUuid 置 null | 元数据含原始 diskOfferingUuid | 注册后为 null | + +### 9.3 快照 VO 映射 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-REG-20 | 快照 parentUuid 保留 | 链式快照 A→B→C | parentUuid 关系完整 | +| UT-REG-21 | SnapshotGroupVO accountUuid 替换 | 元数据含原始 accountUuid | 注册后替换为当前调用者 | +| UT-REG-22 | SnapshotGroupRefVO 路径替换 | volumeSnapshotInstallPath 含旧前缀 | 替换为新前缀 | +| UT-REG-23 | ReferenceVO parentId 统一置 null | 元数据含 parentId=5 | 注册后 parentId=null(C-03-1) | + +--- + +## 10. installPath 前缀替换 + +**覆盖约束**:C-03-3, Part 3 §3.4 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| UT-PATH-01 | sblk VG UUID 替换 | `/dev/123xxx/vol-aaa` → oldPrefix=`/dev/123xxx/`, newPrefix=`/dev/456xxx/` | `/dev/456xxx/vol-aaa` | +| UT-PATH-02 | NFS 挂载路径替换 | `/mnt/old-nfs/vm-data/vol-aaa` → `/mnt/new-nfs/vm-data/vol-aaa` | 替换成功 | +| UT-PATH-03 | 分隔符边界保护 | oldPrefix=`/dev/oldVg`(无尾 `/`) | 替换拒绝或自动补 `/`(C-03-3) | +| UT-PATH-04 | 子串误命中防护 | oldPrefix=`/dev/vg1/`, 路径=`/dev/vg12/vol` | 不匹配,不替换(`startsWith` 精确匹配) | +| UT-PATH-05 | installPath 不匹配 oldPrefix | 路径前缀与 oldPrefix 不一致 | 报错(明确提示哪个路径不匹配) | +| UT-PATH-06 | 批量路径替换一致性 | 10 个 Volume + 20 个 Snapshot 的 installPath | 全部按相同规则替换,无遗漏 | diff --git "a/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" "b/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" new file mode 100644 index 00000000000..d99abf43880 --- /dev/null +++ "b/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" @@ -0,0 +1,276 @@ +# VM 元数据 — 集成测试计划 + +> 集成测试需真实 DB(H2 内存/MySQL)和模拟 Agent,验证跨模块协作正确性。 +> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 + +## 目录 + +1. [sblk 写入与读取](#1-sblk-写入与读取) +2. [local/NFS JSON 读写](#2-localnfs-json-读写) +3. [Poller 端到端流程](#3-poller-端到端流程) +4. [API 拦截器与 markDirty 联动](#4-api-拦截器与-markdirty-联动) +5. [存储迁移元数据链路](#5-存储迁移元数据链路) +6. [注册端到端流程](#6-注册端到端流程) +7. [路径指纹巡检端到端](#7-路径指纹巡检端到端) +8. [API 端到端](#8-api-端到端) + +--- + +## 1. sblk 写入与读取 + +**覆盖约束**:Part 4a–4e, C-01C-2 + +### 1.1 基础写入读取 + +| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | +|---------|------|----------|------|------| +| IT-SBLK-01 | 首次写入 + 读取 | 新建 4MB LV(mock Agent) | writeMetadata(vmUuid, payload) → readMetadata(vmUuid) | 读取内容与写入 payload 完全一致 | +| IT-SBLK-02 | 覆盖写入 A/B Slot 切换 | 已写入 v1 | writeMetadata(v2) → 验证 Header.ActiveSlot 切换 | ActiveSlot 从 0→1(或 1→0) | +| IT-SBLK-03 | 连续 3 次写入后读取 | 空 LV | 写 v1→v2→v3 → readMetadata | 读取到 v3;WriteSequence=3 | +| IT-SBLK-04 | LV 命名格式验证 | — | initializeMetadata(vmUuid) | LV name = `{vm_uuid}_vmmeta`,长度 ≤ 39 字符 | + +### 1.2 Payload 大小与自动扩容 + +| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | +|---------|------|----------|------|------| +| IT-SBLK-10 | 小 payload(1KB) | 4MB LV | 写入 → 读取 | 成功,LV 未扩容 | +| IT-SBLK-11 | payload 超过 4MB LV 容量 | 4MB LV | 写入 2.5MB payload | Agent 触发 `lvextend` → LV 变为 8MB → 写入成功 | +| IT-SBLK-12 | payload 达 30MB 阈值 | 64MB LV | 写入 30MB + 1 payload | 返回 `VM_METADATA_PAYLOAD_TOO_LARGE` 错误 | +| IT-SBLK-13 | 扩容后 Header 布局更新 | 4MB→8MB | 写入 → 读取 Header | SlotA/B Offset 和 Capacity 反映 8MB 布局 | + +### 1.3 op_type 与 storageStructureChange + +| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | +|---------|------|----------|------|------| +| IT-SBLK-20 | CONFIG_UPDATE 写入 | dirty 行 storageStructureChange=false | flush → 观察 Agent 调用 | Agent 收到 op_type=1 (CONFIG_UPDATE) | +| IT-SBLK-21 | STORAGE_CHANGE 写入 | dirty 行 storageStructureChange=true | flush → 观察 Agent 调用 | Agent 收到 op_type=2 (STORAGE_CHANGE) | + +### 1.4 deleteMetadata 幂等性 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-SBLK-30 | 删除存在的 LV | deleteMetadata(vmUuid) | Agent lvremove 成功 | +| IT-SBLK-31 | 删除不存在的 LV | deleteMetadata(vmUuid) | 不抛异常(C-01C-9) | +| IT-SBLK-32 | 双重删除 | delete → delete | 第二次幂等成功 | + +--- + +## 2. local/NFS JSON 读写 + +**覆盖约束**:Part 1c §1.2, C-01C-10 + +### 2.1 基础读写 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-JSON-01 | 首次写入 | writeMetadata(vmUuid, payload) | 创建 `{mountPath}/.zstack-vm-metadata/{vmUuid}.json` | +| IT-JSON-02 | 读取刚写入的文件 | 写入 → readMetadata | 读取内容 == 写入 payload | +| IT-JSON-03 | 覆盖写入 | 写入 v1 → 写入 v2 → 读取 | 读取到 v2 | +| IT-JSON-04 | 容器目录自动创建 | `.zstack-vm-metadata/` 目录不存在 | writeMetadata 自动 mkdir -p(Δ-4) | + +### 2.2 原子写入验证 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-JSON-10 | tmp+fsync+rename 原子性 | 写入过程中观察文件系统 | 先出现 `.sc.tmp` 文件 → rename 后仅有 `.json` | +| IT-JSON-11 | tmp 残留(Agent 重启前) | 手动创建 `.sc.tmp`,Agent 启动 | Agent 启动清理所有 `.sc.tmp` 文件(C-01C-10) | +| IT-JSON-12 | readMetadata 遇到 `.sc.tmp` | 只有 `.sc.tmp` 无 `.json` | 读取返回空/NOT_FOUND(不读 tmp) | + +### 2.3 deleteMetadata 幂等性 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-JSON-20 | 删除存在的 JSON | deleteMetadata → 检查文件 | 文件已删除 | +| IT-JSON-21 | 删除不存在的文件 | 文件本不存在 → deleteMetadata | 不抛异常 | + +--- + +## 3. Poller 端到端流程 + +**覆盖约束**:Part 2 §4, C-CL-02, C-TM-03, C-RB-04 + +### 3.1 正常 flush 链路 + +| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | +|---------|------|----------|------|------| +| IT-POL-01 | markDirty → Poller 认领 → flush → 成功删除 | 一个 UserVm | markDirty(vmUuid) → 等待 Poller 周期 | dirty 行被删除;存储有元数据;PathFingerprint 已记录 | +| IT-POL-02 | 多 VM 并发 flush | 5 个 VM 各有 dirty 行 | Poller 运行 | 5 个 VM 全部 flush 成功 | +| IT-POL-03 | Poller 无 dirty 行时空转 | 无 dirty 行 | Poller 运行 | SELECT 0 rows,正常返回 | +| IT-POL-04 | lastClaimTime 写入 | 认领一行 | 检查 DB | `lastClaimTime` 非 null(C-CL-02) | + +### 3.2 flush 失败与重试 + +| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | +|---------|------|----------|------|------| +| IT-POL-10 | Agent 超时 → 退避重试 | mock Agent 超时 | Poller 认领 → flush 超时 | dirty 行保留,retryCount+1,nextRetryTime 设置退避 | +| IT-POL-11 | 重试 5 次耗尽 | mock Agent 持续失败 | 5 轮 Poller | dirty 行删除 + PathFingerprint.lastFlushFailed=true(C-SR-05) | +| IT-POL-12 | StaleRecoveryTask 重入队 | lastFlushFailed=true | 等待 stale recovery 周期 | markDirty(retryCount=0) + lastFlushFailed=false | + +### 3.3 dirtyVersion 不匹配 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-POL-20 | flush 期间新 markDirty | flush 开始 → 另一线程 markDirty → flush 完成 | onFlushSuccess 检测 dirtyVersion 不匹配 → 释放认领(不删除 dirty 行) | +| IT-POL-21 | flush 后 dirtyVersion 匹配 | 正常 flush | dirty 行删除 | + +### 3.4 并发控制 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-POL-30 | per-VM ChainTask maxPending=1 | 对同一 VM 快速提交 3 次 flush | running=1, pending=1, 第 3 次 exceedMaxPendingCallback 释放 | +| IT-POL-31 | globalFlushInFlight 上限 | mock 11 个 VM flush 中(maxConcurrent=10) | 第 11 个 submitFlushTask 时 AtomicInteger >= max → releaseClaim + 跳过 | +| IT-POL-32 | per-PS syncLevel=5 | 6 个 VM 在同一 PS | 同时只有 5 个在执行 Agent 写入 | + +--- + +## 4. API 拦截器与 markDirty 联动 + +**覆盖约束**:Part 1b, C-PA + +### 4.1 API 成功触发 markDirty + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-INT-01 | APIUpdateVmInstanceMsg(改名) | 发送 API → 成功 | afterCompletion → markDirty(vmUuid, CONFIG) → dirty 行创建 | +| IT-INT-02 | APICreateVolumeSnapshotMsg | 快照创建成功 | markDirty(vmUuid, STORAGE) + storageStructureChange=true | +| IT-INT-03 | APIDeleteVolumeMsg | 删除数据盘成功 | markDirty(vmUuid, STORAGE) | + +### 4.2 API 失败不触发 markDirty + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-INT-10 | APIStopVmInstanceMsg 失败 | mock 停止失败 | afterCompletion 检测 reply.isSuccess()=false → 不 markDirty | +| IT-INT-11 | updateOnFailure=true 的 API 失败 | 批量 API 失败 | 仍然 markDirty | + +### 4.3 pendingApis 超时清理 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-INT-20 | API 45 分钟无 afterCompletion | 注入 pending → 等待 timeout | pendingApis 自动清理 + 补偿 markDirty(C-PA) | +| IT-INT-21 | 正常 API 不超时 | API 正常完成 | pendingApis 正常移除,无超时触发 | + +--- + +## 5. 存储迁移元数据链路 + +**覆盖约束**:Part 1c §1.4, C-01C-4 ~ C-01C-8 + +### 5.1 完整迁移 8 步 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-MIG-01 | sblk→sblk 迁移 | VM 在 PS-A 有元数据 | 8 步全部成功:PS-B 有完整元数据,PS-A 已清理 | +| IT-MIG-02 | local→NFS 迁移 | VM 在 local PS 有 JSON | PS-NFS 有 JSON,local 已清理 | +| IT-MIG-03 | 迁移期间 Poller 暂停 | 迁移开始 | nextRetryTime='2099-12-31',Poller 跳过该 VM | +| IT-MIG-04 | 迁移完成后 Poller 恢复 | 迁移成功 | nextRetryTime=NULL + markDirty(storageStructureChange=true) | + +### 5.2 迁移失败回滚 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-MIG-10 | Step 5 写入失败 | mock 目标 Agent 失败 | 回滚:deleteMetadata(目标) + nextRetryTime=NULL + markDirty(true) | +| IT-MIG-11 | Step 8 清理源端失败 | mock 源 Agent 失败 | WARN 日志,孤儿检测兜底(不阻塞迁移成功) | + +### 5.3 MN 重启恢复 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-MIG-20 | MN 重启时 nextRetryTime=2099 | DB 有暂停行 | managementNodeReady 重置 nextRetryTime=NULL(C-01C-8) | + +--- + +## 6. 注册端到端流程 + +**覆盖约束**:Part 3, C-03-1 ~ C-03-8 + +### 6.1 正常注册 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-REG-01 | 最小 VM 注册(根盘 only) | 有效 metadataContent JSON | VmInstanceVO(state=Stopped) + VolumeVO(Root) 已创建 | +| IT-REG-02 | 含快照链的 VM 注册 | 根盘 + 5 个快照 | 所有 VolumeSnapshotVO 按树结构创建 | +| IT-REG-03 | 含数据盘的 VM 注册 | 根盘 + 2 数据盘 | 3 个 VolumeVO 创建 | +| IT-REG-04 | 注册后 markDirty 触发 | 注册成功 | dirty 行已创建(storageStructureChange=true) | +| IT-REG-05 | 注册后 ConsistencyCheck | 注册成功 | 异步触发 ConsistencyCheck(C-03-7) | +| IT-REG-06 | registered.not.started ResourceConfig | 注册成功 | ResourceConfig 存在 → 后续 API 不触发 markDirty | + +### 6.2 注册拒绝 + +| 用例 ID | 场景 | 输入 | 期望 | +|---------|------|------|------| +| IT-REG-10 | UUID 冲突(正常资源) | vmUuid 已存在且 state≠Registering | 拒绝 + 错误码 | +| IT-REG-11 | 跨存储拒绝 | 根盘在 PS-A,数据盘在 PS-B | `CROSS_STORAGE_REJECTED` + expected/actual PS UUIDs(C-03-2) | +| IT-REG-12 | Root installPath 不存在 | mock Agent 返回 false | BLOCK(拒绝注册)(C-03-6) | +| IT-REG-13 | readStatus=CORRUPTED | metadata.__readStatus="CORRUPTED" | 拒绝注册 | +| IT-REG-14 | schemaVersion 不匹配 | version=999 | 拒绝注册(未设 forceVersionMismatch) | +| IT-REG-15 | forceVersionMismatch=true | version=999 | 允许注册,warnings 列出忽略字段 | + +### 6.3 注册回滚 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-REG-20 | 变基失败触发回滚 | mock qemu-img rebase 失败 | 所有 VO 按"由外到内"删除(C-03-4) | +| IT-REG-21 | Registering 遗留 UUID 冲突 → 回滚重试 | DB 有 state=Registering 的 VM | 自动回滚 → 重新注册 | +| IT-REG-22 | MN 重启扫描 Registering | DB 有 Registering VM | managementNodeReady 触发回滚 | +| IT-REG-23 | 回滚保留 TreeVO(其他 VM 共享) | TreeVO 下有其他 VM 的 ReferenceVO | 仅删除当前 VM 的 ReferenceVO,TreeVO 保留 | + +--- + +## 7. 路径指纹巡检端到端 + +**覆盖约束**:Part 2b §8.2, C-02B-3 + +### 7.1 正常巡检 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-FP-01 | 无漂移 | VM 已 flush + fingerprint 记录 | 巡检通过,不 markDirty | +| IT-FP-02 | installPath 变更 | 手动修改 VolumeVO.installPath | 巡检检测 drift → markDirty | +| IT-FP-03 | keyset 分页遍历 | 510 个 VM(batchSize=500) | 分 2 批遍历完所有 VM | + +### 7.2 边界 + +| 用例 ID | 场景 | 前置条件 | 期望 | +|---------|------|----------|------| +| IT-FP-10 | VM 从未 flush | 无 fingerprint 记录 | 巡检跳过 | +| IT-FP-11 | VM 已销毁(FK CASCADE) | VM 物理删除 | fingerprint 行自动级联删除 | + +--- + +## 8. API 端到端 + +**覆盖约束**:Part 5 + +### 8.1 扫描 API + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-API-01 | 扫描 sblk PS | APIScanVmInstanceMetadataMsg(psUuid) | 返回 vmUuid + vmName + vmCategory 列表 | +| IT-API-02 | 扫描空 PS | 无元数据的 PS | 返回空列表 | + +### 8.2 读取 API + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-API-10 | 读取正常元数据 | APIReadVmInstanceMetadataMsg | readStatus=OK + 完整 JSON | +| IT-API-11 | 读取损坏元数据 | 双 Slot 损坏 | readStatus=CORRUPTED | + +### 8.3 一致性检查 API + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-API-20 | 一致时 | APICheckVmInstanceMetadataConsistencyMsg | 报告一致 | +| IT-API-21 | 不一致 + autoRepair=true | DB 与存储不一致 | 检测到差异 + 自动 markDirty | + +### 8.4 手动更新 API + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-API-30 | APIUpdateVmMetadataMsg | 指定 vmUuid | markDirty 触发 → Poller flush → 存储更新 | + +### 8.5 清理 API + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| IT-API-40 | enabled=false 时清理 | APICleanupVmInstanceMetadataMsg | 存储 + DB 清理成功 | +| IT-API-41 | enabled=true 时拒绝 | 同上 | `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`(C-02B-12) | diff --git "a/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" "b/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" new file mode 100644 index 00000000000..3948d45a99d --- /dev/null +++ "b/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" @@ -0,0 +1,183 @@ +# VM 元数据 — 故障注入测试计划 + +> 故障注入测试验证系统在异常/极端条件下的安全恢复能力。需要 mock Agent 故障、模拟 MN 重启、注入 DB 异常。 +> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 + +## 目录 + +1. [sblk 写入中断与 Crash Recovery](#1-sblk-写入中断与-crash-recovery) +2. [MN 重启恢复](#2-mn-重启恢复) +3. [双 MN 故障转移](#3-双-mn-故障转移) +4. [DB 异常](#4-db-异常) +5. [Agent 异常](#5-agent-异常) +6. [功能开关切换竞态](#6-功能开关切换竞态) + +--- + +## 1. sblk 写入中断与 Crash Recovery + +**覆盖约束**:Part 4c §3 三阶段写入, Part 4d §4.1 崩溃场景矩阵 + +### 1.1 三阶段崩溃点 + +sblk 写入分 3 个阶段:Phase 1(写 Inactive Slot)→ Phase 2(更新 Header: WriteSequence+1, PendingOp 设置)→ Phase 3(切换 ActiveSlot, 清除 PendingOp)。以下测试在每个阶段注入中断。 + +| 用例 ID | 崩溃点 | PendingOp | 恢复后 readStatus | 数据状态 | +|---------|--------|-----------|-------------------|----------| +| FI-SBLK-01 | Phase 1 中断(写 Slot 中途) | 0(Header 未更新) | OK | 旧 Slot 数据完好,未碰旧 Header | +| FI-SBLK-02 | Phase 1 完成、Phase 2 前中断 | 0 | OK | Inactive Slot 有新数据但 Header 未指向它 | +| FI-SBLK-03 | Phase 2 完成、Phase 3 前中断(CONFIG_UPDATE) | 1 | NEED_REPAIR | Inactive Slot 有新数据,Header PendingOp=1 未清除 | +| FI-SBLK-04 | Phase 2 完成、Phase 3 前中断(STORAGE_CHANGE) | 2 | NEED_REPAIR 或 DEGRADED | 取决于 Slot 数据完整性 | +| FI-SBLK-05 | Phase 3 部分写入(Header 4KB 写未完成) | 不确定 | DEGRADED | ControlChecksum 失败 → 降级使用另一 Slot | + +### 1.2 恢复操作 + +| 用例 ID | 场景 | 注入方式 | 步骤 | 期望 | +|---------|------|----------|------|------| +| FI-SBLK-10 | PendingOp=1 恢复 | 手动构造 Header(PendingOp=1, ActiveSlot=0) | readMetadata → 触发 repair | repair 完成 Phase 3 → ActiveSlot 切换 → PendingOp=0 → readStatus=OK 或 RECOVERED | +| FI-SBLK-11 | PendingOp=2 恢复 | 手动构造 Header(PendingOp=2) | readMetadata → 触发 repair | repair 尝试 Phase 3 + read-back 校验 → 成功则 OK,否则 DEGRADED | +| FI-SBLK-12 | 双 Slot 均损坏 | 篡改两个 Slot 的 checksum | readMetadata | readStatus=CORRUPTED | +| FI-SBLK-13 | Active Slot 损坏、Inactive 完好 | 篡改 Active Slot checksum | readMetadata | 降级使用 Inactive Slot → readStatus=DEGRADED | + +### 1.3 LV Extend 期间崩溃 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-SBLK-20 | lvextend 成功、Phase 1 前中断 | 扩容后未写入新数据 | 下次写入基于新布局,旧数据仍通过旧布局可读 | +| FI-SBLK-21 | lvextend 失败(空间不足) | mock lvextend 失败 | 写入返回错误,Header 保持旧布局不变 | +| FI-SBLK-22 | 扩容后读取尝试双布局 | 扩容后 Header 更新前中断 | readMetadata 先尝试 old-layout → 失败 → retry new-layout | + +--- + +## 2. MN 重启恢复 + +### 2.1 Registering 状态清理 + +**覆盖约束**:Part 3 §4 + +| 用例 ID | 场景 | 初始状态 | MN 重启后行为 | 期望 | +|---------|------|----------|--------------|------| +| FI-MN-01 | 本 MN 的 Registering VM | VmInstanceVO(state=Registering, registeringMnUuid=本MN) | managementNodeReady 回滚 | 所有关联 VO 删除(由外到内),VmInstanceVO 删除 | +| FI-MN-02 | 其他 MN(已离线)的 Registering VM | registeringMnUuid=MN-B且MN-B不在线 | managementNodeReady 回滚 | 同上 | +| FI-MN-03 | 其他 MN(仍在线)的 Registering VM | registeringMnUuid=MN-B且MN-B在线 | 跳过 | 不回滚(MN-B 仍在处理) | +| FI-MN-04 | 注册 Step 3 后崩溃(VmInstanceVO + VolumeVO 已创建,快照未创建) | DB 含部分 VO | 回滚 | VolumeVO + VmInstanceVO 删除,无快照残留 | +| FI-MN-05 | 注册 Step 5 后崩溃(所有 VO 已创建,变基未执行) | DB 含全部 VO | 回滚 | 所有 VO 按序删除 | + +### 2.2 Poller 暂停行恢复 + +**覆盖约束**:C-01C-8 + +| 用例 ID | 场景 | 初始状态 | MN 重启后行为 | 期望 | +|---------|------|----------|--------------|------| +| FI-MN-10 | 迁移暂停行存在 | dirty 行 nextRetryTime='2099-12-31T00:00:00' | managementNodeReady 重置 | nextRetryTime=NULL,Poller 恢复处理 | +| FI-MN-11 | 正常退避行不受影响 | dirty 行 nextRetryTime=明天 | managementNodeReady | 不修改(仅匹配 2099 魔数值) | + +### 2.3 lastFlushFailed 恢复链 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-MN-20 | MN 重启后 StaleRecoveryTask 启动 | lastFlushFailed=true 行存在 | StaleRecoveryTask 扫描 → markDirty(retryCount=0) → Poller 重新 flush | +| FI-MN-21 | staleRecoveryCount 达上限 | staleRecoveryCount=10 | StaleRecoveryTask 不再重入队 → WARN 日志 → 等待手动 APIUpdateVmMetadataMsg | + +### 2.4 升级全量刷新 + +**覆盖约束**:Part 2b §9.1 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-MN-30 | 升级刷新中途 MN 崩溃 | 处理到第 500 个 VM 时崩溃 | 重启后 lastRefreshVersion 仍为旧值 → 重新触发全量刷新(Δ-8 保障) | +| FI-MN-31 | 滚动升级 recent-nodeLeft 防护 | MN-A(v2) 启动,15 分钟内有 MN-B(v1) nodeLeft | 延迟 10 分钟重新检查,不立即执行全量刷新 | + +--- + +## 3. 双 MN 故障转移 + +**覆盖约束**:Part 2b §7, C-02B-1, C-02B-2 + +### 3.1 MN 宕机接管 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-HA-01 | MN-A 认领 dirty 行后宕机 | MN-A claim dirty(vm-1) → MN-A 下线 | FK SET NULL → MN-B nodeLeft 延迟 5s → claimAndFlush → vm-1 flush 成功 | +| FI-HA-02 | 接管延迟验证 | 同上 | 总接管时间 ≈ 心跳超时(~30s) + 5s ≈ 35s | +| FI-HA-03 | Fence Check 拦截 zombie 写入 | MN-A GC pause 恢复后尝试写入 | dirty 行 managementNodeUuid 已不是 MN-A → abort(C-02B-2) | + +### 3.2 脑裂防护 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-HA-10 | 两个 MN 同时 CAS 认领同一行 | 并发 UPDATE WHERE uuid=x AND managementNodeUuid IS NULL | 只有一个 affected_rows=1,另一个=0 | +| FI-HA-11 | GC pause 期间对端接管后并发写入 | MN-A pause → MN-B 接管写入 → MN-A 恢复写入 | sblk WriteSequence 保证最终一致(更高 SeqNum 胜出) | + +### 3.3 nodeLeft 延迟配置 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-HA-20 | nodeLeft.delaySec=0 | 配置延迟为 0 | 立即接管(增大竞态风险,仅验证可配置性) | +| FI-HA-21 | nodeLeft.delaySec=10 | 配置延迟为 10s | 10s 后接管 | + +--- + +## 4. DB 异常 + +### 4.1 markDirty 并发竞态 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-DB-01 | INSERT IGNORE 竞态(两个 MN 同时 INSERT 同一 VM) | 并发 markDirty | 一个 INSERT 成功,一个 IGNORE → 两者 UPDATE 均安全(C-DM-01) | +| FI-DB-02 | INSERT=0 且 UPDATE=0 | 极端竞态:INSERT IGNORE 后 UPDATE 前行被删除 | 重新 INSERT IGNORE(C-DM-01 保障) | + +### 4.2 FK CASCADE 验证 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-DB-10 | VM 物理删除级联清理 dirty 行 | DELETE VmInstanceEO | VmMetadataDirtyVO 行自动删除 | +| FI-DB-11 | VM 物理删除级联清理 fingerprint | DELETE VmInstanceEO | VmMetadataPathFingerprintVO 行自动删除 | +| FI-DB-12 | MN 离线级联释放认领 | DELETE ManagementNodeVO | dirty 行 managementNodeUuid=NULL(FK SET_NULL) | + +--- + +## 5. Agent 异常 + +### 5.1 Agent 不可达 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-AGT-01 | 写入时 Agent 超时 | mock Agent 不响应 | UpdateVmInstanceMetadataOnHypervisorMsg 超时(2min) → onFlushFailure | +| FI-AGT-02 | Agent 返回未知错误码 | mock Agent 返回 500 | onFlushFailure → 进入退避 | +| FI-AGT-03 | 扫描时 Agent 超时 | APIScanVmInstanceMetadataMsg + mock Agent 不响应 | API 超时返回错误 | + +### 5.2 Agent 部分成功 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-AGT-10 | 写入 Agent 成功但 MN 在收到响应前崩溃 | mock:Agent 写入完成 → MN 崩溃 | 存储有新数据但 dirty 行未删除 → MN 重启后 Poller 重新 flush(幂等覆盖写) | +| FI-AGT-11 | 读取 Agent 返回损坏数据 | mock Agent 返回非 JSON | readMetadata 报错,readStatus=CORRUPTED | + +### 5.3 PS 不可达 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-AGT-20 | PS 卸载期间 flush | PS Detached | flush 失败 → 退避 → stale → 最终熔断(staleRecoveryCount >= maxCycles) | +| FI-AGT-21 | PS 重新挂载后恢复 | PS Reattach + API 触发 | markDirty → Poller flush 成功 | + +--- + +## 6. 功能开关切换竞态 + +**覆盖约束**:Part 2b §9a, C-02B-11 ~ C-02B-13 + +### 6.1 快速 toggle + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-TOG-01 | false→true→false 快速切换 | 启用后立即禁用 | 初始化任务检测到 enabled=false → 中止(C-02B-13) | +| FI-TOG-02 | false→true→false→true | 两次启用 | 第二次初始化:LEFT JOIN 排除已有 dirty 行 → 仅初始化新 VM | + +### 6.2 true→false 清理 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| FI-TOG-10 | 禁用时清理 PathFingerprint | true→false | 异步批量删除所有 VmMetadataPathFingerprintVO(Δ-10) | +| FI-TOG-11 | 禁用时 dirty 行保留 | true→false | VmMetadataDirtyVO 行不删除 | +| FI-TOG-12 | 禁用时存储元数据保留 | true→false | sblk LV / JSON 文件不删除 | diff --git "a/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" "b/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" new file mode 100644 index 00000000000..97228f7dd1d --- /dev/null +++ "b/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" @@ -0,0 +1,261 @@ +# VM 元数据 — 性能与补充测试计划 + +> 性能基准测试和未归入前三类的补充测试场景。 +> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md)。 + +## 目录 + +1. [性能基准:全量元数据更新](#1-性能基准全量元数据更新) +2. [性能基准:升级批次压力](#2-性能基准升级批次压力) +3. [性能基准:注册流程](#3-性能基准注册流程) +4. [性能基准:Poller 吞吐](#4-性能基准poller-吞吐) +5. [性能基准:sblk 读写延迟](#5-性能基准sblk-读写延迟) +6. [补充:E2E 场景测试](#6-补充e2e-场景测试) +7. [补充:数据迁移与兼容性](#7-补充数据迁移与兼容性) +8. [补充:安全与权限](#8-补充安全与权限) +9. [补充:可观测性验证](#9-补充可观测性验证) +10. [补充:GlobalConfig 动态生效](#10-补充globalconfig-动态生效) + +--- + +## 1. 性能基准:全量元数据更新 + +### 1.1 1000 VM 全量 markDirty + +| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | +|---------|------|------|----------|------------| +| PERF-01 | 1000 VM 批量 markDirty 耗时 | 单 MN,MySQL | markDirty 全部完成时间 | < 10s | +| PERF-02 | 1000 VM 批量 markDirty(Galera 双节点) | 双 MN | 同上 + 无死锁 | < 15s | +| PERF-03 | 1000 VM markDirty 后 Poller 全部消化 | pollInterval=5s, maxConcurrent=10, ps.maxConcurrent=5 | 从首个 markDirty 到最后一个 dirty 行删除 | < 15 分钟 | + +### 1.2 10000 VM 大规模验证 + +| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | +|---------|------|------|----------|------------| +| PERF-04 | 10000 VM markDirty | 双 MN | 插入完成时间 | < 60s | +| PERF-05 | 10000 VM Poller 消化 | 双 MN, maxConcurrent=10 | 全部 flush 完成时间 | < 2.5 小时 | + +### 1.3 单 VM flush 延迟分布 + +| 用例 ID | 场景 | 度量指标 | 基准 | +|---------|------|----------|------| +| PERF-06 | 普通 VM(1 根盘、少量快照)flush 延迟 | buildMetadata + Agent 写入总耗时 | P50 < 500ms, P99 < 3s | +| PERF-07 | 大 VM(24 盘、256 快照)flush 延迟 | 同上 | P50 < 5s, P99 < 15s | + +--- + +## 2. 性能基准:升级批次压力 + +**覆盖约束**:Part 2b §9.2, C-02B-4 + +### 2.1 升级全量刷新 + +| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | +|---------|------|------|----------|------------| +| PERF-10 | 1000 VM 升级全量 markDirty | batchSize=1000 | INSERT IGNORE + UPDATE 总耗时 | < 5s | +| PERF-11 | 10000 VM 升级全量 markDirty | batchSize=1000 | 10 批总耗时 | < 30s | +| PERF-12 | 升级全量 markDirty 期间业务 API 影响 | PERF-11 同时运行 100 个 API | API 响应延迟增幅 | < 20% | + +### 2.2 false→true 初始化 + +| 用例 ID | 场景 | 配置 | 度量指标 | 基准 | +|---------|------|------|----------|------| +| PERF-15 | 5000 VM 初始化 | initBatchSize=200, batchDelay=5s | 初始化完成时间 | ≈ 25 批 × 5s = ~125s | +| PERF-16 | 初始化期间 Poller 吞吐 | 同上 | dirty 行积压量峰值 | < 500 行(证明批间延迟有效) | + +--- + +## 3. 性能基准:注册流程 + +### 3.1 注册耗时 + +| 用例 ID | 场景 | 输入规模 | 度量指标 | 基准(P95) | +|---------|------|----------|----------|------------| +| PERF-20 | 最小 VM 注册 | 1 根盘、0 快照 | 注册总耗时(Step 1-7) | < 3s | +| PERF-21 | 中等 VM 注册 | 4 盘、50 快照 | 同上 | < 10s | +| PERF-22 | 极端 VM 注册 | 24 盘、256 快照 + Group + Ref | 同上 | < 60s | +| PERF-23 | 极端 VM UUID 冲突检测 | ~7000 UUID(分批 1000/批) | 冲突检测耗时 | < 2s | + +### 3.2 注册回滚耗时 + +| 用例 ID | 场景 | 输入规模 | 度量指标 | 基准 | +|---------|------|----------|----------|------| +| PERF-25 | 大 VM 回滚 | 24 盘、256 快照全部已创建 | 由外到内删除总耗时 | < 30s | + +--- + +## 4. 性能基准:Poller 吞吐 + +### 4.1 Poller 轮询效率 + +| 用例 ID | 场景 | 度量指标 | 基准 | +|---------|------|----------|------| +| PERF-30 | 空 Poller 周期(0 dirty 行) | SELECT 查询耗时 | < 1ms | +| PERF-31 | 满载 Poller 周期(50 行认领) | claim + submit 总耗时 | < 100ms | +| PERF-32 | 大量退避行跳过 | 500 dirty 行中 450 行有 nextRetryTime > now | WHERE 过滤效率 | < 5ms | + +### 4.2 路径指纹巡检效率 + +| 用例 ID | 场景 | 度量指标 | 基准 | +|---------|------|----------|------| +| PERF-35 | 1000 VM 巡检(无 drift) | 全量巡检耗时 | < 5s | +| PERF-36 | 5000 VM 巡检 keyset 分页 | 分页查询 + 比对总耗时 | < 20s | +| PERF-37 | 巡检期间零存储 I/O 验证 | Agent 调用计数 | 0 次 Agent 调用 | + +--- + +## 5. 性能基准:sblk 读写延迟 + +### 5.1 写入延迟 + +| 用例 ID | 场景 | Payload 大小 | 度量指标 | 基准 | +|---------|------|-------------|----------|------| +| PERF-40 | 小 payload 写入 | 10KB | Agent pwrite 耗时 | < 10ms | +| PERF-41 | 中等 payload 写入 | 500KB | 同上 | < 50ms | +| PERF-42 | 大 payload 写入 | 5MB | 同上(含 lvextend) | < 500ms | + +### 5.2 读取延迟 + +| 用例 ID | 场景 | 度量指标 | 基准 | +|---------|------|----------|------| +| PERF-45 | 正常读取(PendingOp=0) | pread + 解析耗时 | < 20ms | +| PERF-46 | 带 repair 的读取(PendingOp=1) | repair + pread 总耗时 | < 100ms | + +### 5.3 扫描效率 + +| 用例 ID | 场景 | 度量指标 | 基准 | +|---------|------|----------|------| +| PERF-50 | 100 LV 扫描(仅读 Header 摘要区) | scanMetadataVmUuids 总耗时 | < 2s | +| PERF-51 | 1000 LV 扫描 | 同上 | < 15s | + +--- + +## 6. 补充:E2E 场景测试 + +### 6.1 完整生命周期 + +| 用例 ID | 场景 | 步骤 | 验证点 | +|---------|------|------|--------| +| E2E-01 | VM 创建→运行→改名→加盘→快照→迁移→销毁 | 全流程 | 每步后元数据正确反映 DB 状态 | +| E2E-02 | VM 创建→销毁→恢复→再销毁→Expunge | Destroy→Recover→Destroy→Expunge | Recover 后元数据恢复更新;Expunge 时 deleteMetadata | +| E2E-03 | 链式克隆子 VM 注册 | 从存储扫描→读取→注册子 VM | ReferenceVO(parentId=null) + TreeVO 幂等 | +| E2E-04 | VM 注册→首次启动→markDirty 触发 | 注册完成→启动 VM→Running | `registered.not.started` Config 删除 → markDirty → Poller flush | + +### 6.2 多 MN 协同场景 + +| 用例 ID | 场景 | 步骤 | 验证点 | +|---------|------|------|--------| +| E2E-10 | 双 MN 分摊 dirty 行处理 | 20 个 VM markDirty + 双 MN Poller | 所有 VM 最终 flush 成功,无遗漏 | +| E2E-11 | MN-A flush 中 → MN-A 宕机 → MN-B 接管 | in-flight flush 场景 | MN-B 接管并成功 flush | + +### 6.3 存储迁移 + 元数据联动 + +| 用例 ID | 场景 | 步骤 | 验证点 | +|---------|------|------|--------| +| E2E-20 | 根盘迁移 sblk→sblk | 迁移成功后读取目标 PS 元数据 | 元数据内容完整 + 源 PS 已清理 | +| E2E-21 | 根盘迁移失败回滚 | Step 5 写入失败 | 源 PS 元数据不变 + 目标 PS 残留清理 + Poller 恢复 | +| E2E-22 | 仅数据盘迁移 | 数据盘从 PS-A → PS-B(根盘不动) | 元数据更新到根盘所在 PS,storageStructureChange=true | + +--- + +## 7. 补充:数据迁移与兼容性 + +### 7.1 版本兼容 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| COMPAT-01 | 旧版 ZStack 写入的 sblk → 新版读取 | 用旧格式写入 → 新版 readMetadata | 正常读取(schemaVersion 向后兼容) | +| COMPAT-02 | schemaVersion 低于当前的元数据注册 | forceVersionMismatch=true | 注册成功 + warnings 列出差异字段 | +| COMPAT-03 | schemaVersion 高于当前的元数据注册 | 来自更新版本的 JSON | 默认拒绝;forceVersionMismatch=true 时允许 | + +### 7.2 DB 升级 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| COMPAT-10 | 首次部署(新建表) | 全新安装 | VmMetadataDirtyVO + VmMetadataPathFingerprintVO 表创建成功 | +| COMPAT-11 | 升级部署(ALTER TABLE) | 从无元数据版本升级 | 新表正确创建;GlobalConfig 默认值生效 | + +--- + +## 8. 补充:安全与权限 + +### 8.1 API 权限 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| SEC-01 | 普通用户调用 APIRegisterVmInstanceFromMetadataMsg | 非 admin 账户 | 权限拒绝 | +| SEC-02 | 普通用户查询 Registering VM | QueryVmInstance | Registering VM 不可见 | +| SEC-03 | admin 查询 Registering VM | QueryVmInstance | Registering VM 可见 | +| SEC-04 | 普通用户调用 APIScanVmInstanceMetadataMsg | 非 admin 账户 | 权限拒绝 | + +### 8.2 注册安全 + +| 用例 ID | 场景 | 步骤 | 期望 | +|---------|------|------|------| +| SEC-10 | 恶意 JSON 注入 | metadataContent 含 SQL 注入 payload | ORM 参数化查询隔离,不影响 DB | +| SEC-11 | 超大 JSON(> 100MB) | metadataContent 超大 | API 层大小限制拦截 | +| SEC-12 | installPath 路径遍历 | installPath 含 `../../etc/passwd` | 前缀锚定替换 + 正则预校验拦截(C-03-3) | + +--- + +## 9. 补充:可观测性验证 + +**覆盖约束**:Part 2b §14 + +### 9.1 Prometheus 指标 + +| 用例 ID | 指标 | 场景 | 期望 | +|---------|------|------|------| +| OBS-01 | `vm_metadata_flush_total{status=success}` | 正常 flush | Counter 递增 | +| OBS-02 | `vm_metadata_flush_total{status=fail}` | Agent 失败 | Counter 递增 | +| OBS-03 | `vm_metadata_flush_duration_seconds` | 正常 flush | Histogram 记录耗时 | +| OBS-04 | `vm_metadata_dirty_queue_size` | markDirty 后 | Gauge > 0 | +| OBS-05 | `vm_metadata_registration_total{status=success}` | 注册成功 | Counter 递增 | +| OBS-06 | `vm_metadata_registration_total{status=rollback}` | 注册回滚 | Counter 递增 | + +### 9.2 日志验证 + +| 用例 ID | 场景 | 期望日志 | +|---------|------|----------| +| OBS-10 | flush 失败且重试耗尽 | ERROR 日志含 vmUuid + 失败原因 + retryCount | +| OBS-11 | Fence Check 拦截 | WARN `"Lost claim on vm {uuid}, abort flush write"` | +| OBS-12 | 路径漂移检测 | WARN `"path drift detected for VM [{uuid}]"` + 新旧 snapshot 对比 | +| OBS-13 | 孤儿元数据检测 | WARN `"orphan metadata detected: ps={}, vm={}, reason={}"` | +| OBS-14 | stale recovery 熔断 | WARN `"VM [{}] metadata stale recovery exceeded {} cycles, entering permanent-stale"` | + +--- + +## 10. 补充:GlobalConfig 动态生效 + +**覆盖约束**:C-RB-04, C-M4, 各 §13 配置项 + +| 用例 ID | 配置项 | 变更方式 | 期望 | +|---------|--------|----------|------| +| CFG-01 | `vm.metadata.dirty.pollIntervalSec` | 5→10 | 下轮 Poller 间隔变为 10s | +| CFG-02 | `vm.metadata.maxRetry` | 5→3 | 3 次失败后即标记 stale | +| CFG-03 | `vm.metadata.global.maxConcurrent` | 10→5 | AtomicInteger 上限立即生效 | +| CFG-04 | `vm.metadata.retry.baseDelaySeconds` | 10→20 | 退避间隔加倍 | +| CFG-05 | `vm.metadata.nodeLeft.delaySec` | 5→10 | nodeLeft 事件后延迟 10s 再接管 | +| CFG-06 | `vm.metadata.enabled` | true→false | Poller 停止处理 + PathFingerprint 异步清理 | +| CFG-07 | `vm.metadata.enabled` | false→true | 分批初始化启动 | +| CFG-08 | `vm.metadata.pendingApi.timeoutMinutes` | 45→30 | pendingApis 超时缩短 | +| CFG-09 | `vm.metadata.pathCheck.intervalSec` | 300→60 | 巡检频率加快 | +| CFG-10 | `vm.metadata.staleRecovery.maxCycles` | 10→3 | 熔断更快触发 | + +--- + +## 附录:测试用例 ID 编号规则 + +| 前缀 | 类别 | 文档 | +|------|------|------| +| UT-* | 单元测试 | Part 7a | +| IT-* | 集成测试 | Part 7b | +| FI-* | 故障注入测试 | Part 7c | +| PERF-* | 性能基准测试 | Part 7d | +| E2E-* | 端到端场景测试 | Part 7d §6 | +| COMPAT-* | 兼容性测试 | Part 7d §7 | +| SEC-* | 安全与权限测试 | Part 7d §8 | +| OBS-* | 可观测性验证 | Part 7d §9 | +| CFG-* | 配置动态生效 | Part 7d §10 | + +**总计**:约 **190+ 条测试用例**,覆盖序列化、存储协议、并发控制、故障恢复、性能基准、安全权限和可观测性全维度。 From f12598a255b67fc3b01a587015a49b904bb31687 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 11:52:48 +0800 Subject: [PATCH 09/10] [vm-metadata]: normalize file modes for Reimage API files - APIReimageVmInstanceEvent.java: 755 -> 644 - APIReimageVmInstanceEventDoc_zh_cn.groovy: 755 -> 644 --- .../main/java/org/zstack/header/vm/APIReimageVmInstanceEvent.java | 0 .../zstack/header/vm/APIReimageVmInstanceEventDoc_zh_cn.groovy | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEvent.java mode change 100755 => 100644 header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEventDoc_zh_cn.groovy diff --git a/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEvent.java b/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEvent.java old mode 100755 new mode 100644 diff --git a/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceEventDoc_zh_cn.groovy old mode 100755 new mode 100644 From 0acdf4636458f97e8e71a48b41708e70018c58d9 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 12 Mar 2026 13:52:08 +0800 Subject: [PATCH 10/10] [vm-metadata]: annotate all APIMessage subclasses with @MetadataImpact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scan all APIMessage subclasses across zstack (excl premium) and add @MetadataImpact annotations per design doc vm-metadata-01b §4: - STORAGE (17 APIs): volume attach/detach/delete/recover, reimage, storage migration, snapshot CRUD, clone, template VM - CONFIG (22 APIs): systemtag CRUD, resourceconfig, update/recover VM, VM NIC ops, boot/ssh/hostname/qga settings, resize volume, convert template - NONE (513 APIs): all remaining APIMessage subclasses Classification rules derived from vm-metadata-01b-API拦截与VM解析.md §4. CI will enforce that all APIMessage subclasses carry this annotation. Resolves: ZSV-10000 --- .../core/captcha/APIRefreshCaptchaMsg.java | 2 + .../config/APIGetGlobalConfigOptionsMsg.java | 2 + .../core/config/APIQueryGlobalConfigMsg.java | 2 + .../core/config/APIResetGlobalConfigMsg.java | 2 + .../core/config/APIUpdateGlobalConfigMsg.java | 2 + .../zstack/core/debug/APICleanQueueMsg.java | 2 + .../zstack/core/debug/APIDebugSignalMsg.java | 2 + .../core/debug/APIGetDebugSignalMsg.java | 2 + .../APICheckElaborationContentMsg.java | 2 + .../APIGetElaborationCategoriesMsg.java | 2 + .../core/errorcode/APIGetElaborationsMsg.java | 2 + .../errorcode/APIReloadElaborationMsg.java | 2 + .../core/eventlog/APIQueryEventLogMsg.java | 2 + .../org/zstack/core/gc/APIDeleteGCJobMsg.java | 2 + .../org/zstack/core/gc/APIQueryGCJobMsg.java | 2 + .../zstack/core/gc/APITriggerGCJobMsg.java | 2 + ...46\346\235\237\347\264\242\345\274\225.md" | 94 -- ...16\345\272\217\345\210\227\345\214\226.md" | 417 -------- ...\344\270\216VM\350\247\243\346\236\220.md" | 433 -------- ...77\350\231\232\346\213\237\346\234\272.md" | 464 --------- ...\240\207\350\256\260\344\270\216Poller.md" | 939 ----------------- ...50\344\270\216\350\277\220\347\273\264.md" | 942 ------------------ ...14\344\270\216\350\277\220\347\273\264.md" | 572 ----------- ...17\350\256\256\346\246\202\350\277\260.md" | 248 ----- ...33\345\210\266\345\270\203\345\261\200.md" | 289 ------ ...31\345\205\245\346\265\201\347\250\213.md" | 268 ----- ...26\344\270\216\346\201\242\345\244\215.md" | 441 -------- ...\350\277\220\347\273\264\344\270\216IO.md" | 432 -------- ...etadata-05-API\350\256\276\350\256\241.md" | 521 ---------- ...13\350\257\225\350\256\241\345\210\222.md" | 278 ------ ...13\350\257\225\350\256\241\345\210\222.md" | 276 ----- ...50\345\205\245\346\265\213\350\257\225.md" | 183 ---- ...45\345\205\205\346\265\213\350\257\225.md" | 261 ----- .../header/APIIsOpensourceVersionMsg.java | 2 + .../allocator/APIGetCpuMemoryCapacityMsg.java | 2 + .../APIGetHostAllocatorStrategiesMsg.java | 2 + .../header/apimediator/APIIsReadyToGoMsg.java | 2 + .../cluster/APIChangeClusterStateMsg.java | 2 + .../header/cluster/APICreateClusterMsg.java | 2 + .../header/cluster/APIDeleteClusterMsg.java | 2 + .../header/cluster/APIQueryClusterMsg.java | 2 + .../header/cluster/APIUpdateClusterMsg.java | 2 + .../header/cluster/APIUpdateClusterOSMsg.java | 2 + .../APIChangeDiskOfferingStateMsg.java | 2 + .../APIChangeInstanceOfferingStateMsg.java | 2 + .../APICreateDiskOfferingMsg.java | 2 + .../APICreateInstanceOfferingMsg.java | 2 + .../APIDeleteDiskOfferingMsg.java | 2 + .../APIDeleteInstanceOfferingMsg.java | 2 + .../APIGenerateApiJsonTemplateMsg.java | 3 + ...APIGenerateApiTypeScriptDefinitionMsg.java | 2 + .../APIGenerateGroovyClassMsg.java | 2 + .../APIGenerateSqlForeignKeyMsg.java | 2 + .../configuration/APIGenerateSqlIndexMsg.java | 2 + .../APIGenerateSqlVOViewMsg.java | 2 + .../APIGenerateTestLinkDocumentMsg.java | 3 + .../APIGetGlobalPropertyMsg.java | 2 + .../APIQueryDiskOfferingMsg.java | 2 + .../APIQueryInstanceOfferingMsg.java | 2 + .../APIUpdateDiskOfferingMsg.java | 2 + .../APIUpdateInstanceOfferingMsg.java | 2 + .../console/APIQueryConsoleProxyAgentMsg.java | 2 + .../APIReconnectConsoleProxyAgentMsg.java | 2 + .../console/APIRequestConsoleAccessMsg.java | 2 + .../APIUpdateConsoleProxyAgentMsg.java | 2 + .../header/core/APIGetChainTaskMsg.java | 2 + .../core/encrypt/APIGetEncryptedFieldMsg.java | 2 + .../core/encrypt/APIUpdateEncryptKeyMsg.java | 2 + .../service/APIGetExternalServicesMsg.java | 2 + .../service/APIReloadExternalServiceMsg.java | 2 + .../core/progress/APIGetTaskProgressMsg.java | 2 + .../core/webhooks/APICreateWebhookMsg.java | 2 + .../core/webhooks/APIDeleteWebhookMsg.java | 2 + .../core/webhooks/APIQueryWebhookMsg.java | 2 + .../core/webhooks/APIUpdateWebhookMsg.java | 2 + .../org/zstack/header/host/APIAddHostMsg.java | 3 + .../header/host/APIChangeHostStateMsg.java | 2 + .../zstack/header/host/APIDeleteHostMsg.java | 2 + .../host/APIGetHostBlockDevicesMsg.java | 2 + .../header/host/APIGetHostPowerStatusMsg.java | 2 + .../header/host/APIGetHostSensorsMsg.java | 2 + .../zstack/header/host/APIGetHostTaskMsg.java | 2 + .../header/host/APIGetHostWebSshUrlMsg.java | 2 + .../header/host/APIGetHypervisorTypesMsg.java | 2 + .../APIGetPhysicalMachineBlockDevicesMsg.java | 2 + .../header/host/APIMountBlockDeviceMsg.java | 2 + .../zstack/header/host/APIPowerOnHostMsg.java | 2 + .../header/host/APIPowerResetHostMsg.java | 2 + .../zstack/header/host/APIQueryHostMsg.java | 2 + .../header/host/APIReconnectHostMsg.java | 2 + .../header/host/APIShutdownHostMsg.java | 2 + .../header/host/APIUpdateHostIpmiMsg.java | 2 + .../zstack/header/host/APIUpdateHostMsg.java | 2 + .../header/host/APIUpdateHostNqnMsg.java | 2 + .../header/host/APIUpdateHostnameMsg.java | 2 + .../identity/APIChangeResourceOwnerMsg.java | 2 + .../header/identity/APICreateAccountMsg.java | 2 + .../header/identity/APIDeleteAccountMsg.java | 2 + .../identity/APIGetAccountQuotaUsageMsg.java | 2 + .../identity/APIGetResourceAccountMsg.java | 2 + .../header/identity/APILogInByAccountMsg.java | 2 + .../zstack/header/identity/APILogOutMsg.java | 2 + .../header/identity/APIQueryAccountMsg.java | 2 + .../APIQueryAccountResourceRefMsg.java | 2 + .../header/identity/APIQueryQuotaMsg.java | 2 + .../header/identity/APIRenewSessionMsg.java | 2 + .../identity/APIRevokeResourceSharingMsg.java | 2 + .../header/identity/APIShareResourceMsg.java | 2 + .../header/identity/APIUpdateAccountMsg.java | 2 + .../header/identity/APIUpdateQuotaMsg.java | 2 + .../identity/APIValidateSessionMsg.java | 2 + .../login/APIGetLoginProceduresMsg.java | 2 + .../header/identity/login/APILogInMsg.java | 2 + .../role/api/APIAttachRoleToAccountMsg.java | 2 + .../identity/role/api/APICreateRoleMsg.java | 2 + .../identity/role/api/APIDeleteRoleMsg.java | 2 + .../role/api/APIDetachRoleFromAccountMsg.java | 2 + .../role/api/APIGetRolePolicyActionsMsg.java | 2 + .../role/api/APIQueryRoleAccountRefMsg.java | 2 + .../identity/role/api/APIQueryRoleMsg.java | 2 + .../identity/role/api/APIUpdateRoleMsg.java | 2 + .../zstack/header/image/APIAddImageMsg.java | 2 + .../image/APICalculateImageHashMsg.java | 2 + .../header/image/APIChangeImageStateMsg.java | 2 + ...CreateDataVolumeTemplateFromVolumeMsg.java | 2 + ...taVolumeTemplateFromVolumeSnapshotMsg.java | 2 + ...teRootVolumeTemplateFromRootVolumeMsg.java | 2 + ...otVolumeTemplateFromVolumeSnapshotMsg.java | 2 + .../header/image/APIDeleteImageMsg.java | 2 + .../header/image/APIExpungeImageMsg.java | 2 + ...idateBackupStorageForCreatingImageMsg.java | 2 + ...APIGetCandidateImagesForCreatingVmMsg.java | 2 + .../image/APIGetUploadImageJobDetailsMsg.java | 2 + .../zstack/header/image/APIQueryImageMsg.java | 2 + .../header/image/APIRecoverImageMsg.java | 2 + .../header/image/APISetImageBootModeMsg.java | 2 + .../header/image/APISyncImageSizeMsg.java | 2 + .../header/image/APIUpdateImageMsg.java | 2 + .../header/longjob/APICancelLongJobMsg.java | 2 + .../header/longjob/APICleanLongJobMsg.java | 2 + .../header/longjob/APIDeleteLongJobMsg.java | 2 + .../header/longjob/APIQueryLongJobMsg.java | 2 + .../header/longjob/APIRerunLongJobMsg.java | 2 + .../header/longjob/APIResumeLongJobMsg.java | 2 + .../header/longjob/APISubmitLongJobMsg.java | 2 + .../header/longjob/APIUpdateLongJobMsg.java | 2 + .../managementnode/APIGetCurrentTimeMsg.java | 2 + .../APIGetManagementNodeArchMsg.java | 2 + .../APIGetManagementNodeOSMsg.java | 2 + .../APIGetPlatformTimeZoneMsg.java | 2 + .../managementnode/APIGetSupportAPIsMsg.java | 2 + .../managementnode/APIGetVersionMsg.java | 2 + .../APIQueryManagementNodeMsg.java | 2 + .../l2/APIAttachL2NetworkToClusterMsg.java | 2 + .../l2/APIAttachL2NetworkToHostMsg.java | 2 + .../network/l2/APICreateL2NetworkMsg.java | 3 + .../l2/APICreateL2NoVlanNetworkMsg.java | 2 + .../network/l2/APICreateL2VlanNetworkMsg.java | 2 + .../network/l2/APIDeleteL2NetworkMsg.java | 2 + .../l2/APIDetachL2NetworkFromClusterMsg.java | 2 + .../l2/APIDetachL2NetworkFromHostMsg.java | 2 + ...idateClustersForAttachingL2NetworkMsg.java | 2 + ...idateL2NetworksForAttachingClusterMsg.java | 2 + .../network/l2/APIGetL2NetworkTypesMsg.java | 2 + .../network/l2/APIGetVSwitchTypesMsg.java | 2 + .../network/l2/APIQueryL2NetworkMsg.java | 2 + .../network/l2/APIQueryL2VlanNetworkMsg.java | 2 + .../network/l2/APIUpdateL2NetworkMsg.java | 2 + ...APIUpdateL2NetworkVirtualNetworkIdMsg.java | 2 + .../network/l3/APIAddDnsToL3NetworkMsg.java | 2 + .../l3/APIAddHostRouteToL3NetworkMsg.java | 2 + .../l3/APIAddIpRangeByNetworkCidrMsg.java | 2 + .../header/network/l3/APIAddIpRangeMsg.java | 2 + .../l3/APIAddIpv6RangeByNetworkCidrMsg.java | 2 + .../header/network/l3/APIAddIpv6RangeMsg.java | 2 + .../network/l3/APIAddReservedIpRangeMsg.java | 2 + .../l3/APIChangeL3NetworkStateMsg.java | 2 + .../network/l3/APICheckIpAvailabilityMsg.java | 2 + .../network/l3/APICreateL3NetworkMsg.java | 2 + .../network/l3/APIDeleteIpAddressMsg.java | 2 + .../network/l3/APIDeleteIpRangeMsg.java | 2 + .../network/l3/APIDeleteL3NetworkMsg.java | 2 + .../l3/APIDeleteReservedIpRangeMsg.java | 2 + .../header/network/l3/APIGetFreeIpMsg.java | 2 + .../l3/APIGetIpAddressCapacityMsg.java | 2 + .../network/l3/APIGetL3NetworkMtuMsg.java | 2 + .../APIGetL3NetworkRouterInterfaceIpMsg.java | 2 + .../network/l3/APIGetL3NetworkTypesMsg.java | 2 + .../network/l3/APIQueryAddressPoolMsg.java | 2 + .../network/l3/APIQueryIpAddressMsg.java | 2 + .../header/network/l3/APIQueryIpRangeMsg.java | 2 + .../network/l3/APIQueryL3NetworkMsg.java | 2 + .../l3/APIRemoveDnsFromL3NetworkMsg.java | 2 + .../APIRemoveHostRouteFromL3NetworkMsg.java | 2 + .../network/l3/APISetL3NetworkMtuMsg.java | 2 + .../APISetL3NetworkRouterInterfaceIpMsg.java | 2 + .../network/l3/APIUpdateIpRangeMsg.java | 2 + .../network/l3/APIUpdateL3NetworkMsg.java | 2 + .../APIAddNetworkServiceProviderMsg.java | 3 + ...hNetworkServiceProviderToL2NetworkMsg.java | 3 + ...APIAttachNetworkServiceToL3NetworkMsg.java | 2 + ...IDetachNetworkServiceFromL3NetworkMsg.java | 2 + ...etworkServiceProviderFromL2NetworkMsg.java | 3 + .../service/APIGetNetworkServiceTypesMsg.java | 2 + ...APIQueryNetworkServiceL3NetworkRefMsg.java | 2 + .../APIQueryNetworkServiceProviderMsg.java | 2 + .../APIGenerateInventoryQueryDetailsMsg.java | 3 + .../query/APIGenerateQueryableFieldsMsg.java | 2 + .../api/APICreateResourceAttributeKeyMsg.java | 2 + .../APICreateResourceAttributeValueMsg.java | 2 + .../api/APIDeleteResourceAttributeKeyMsg.java | 2 + .../APIDeleteResourceAttributeValueMsg.java | 2 + .../api/APIQueryResourceAttributeKeyMsg.java | 2 + .../APIQueryResourceAttributeValueMsg.java | 2 + .../api/APIUpdateResourceAttributeKeyMsg.java | 2 + .../search/APICreateSearchIndexMsg.java | 3 + .../search/APIDeleteSearchIndexMsg.java | 3 + .../APIAddExternalBackupStorageMsg.java | 2 + .../APIAddExternalPrimaryStorageMsg.java | 2 + .../APIDiscoverExternalPrimaryStorageMsg.java | 2 + .../APIUpdateExternalPrimaryStorageMsg.java | 2 + .../backup/APIAddBackupStorageMsg.java | 3 + .../APIAttachBackupStorageToZoneMsg.java | 2 + .../APIChangeBackupStorageStateMsg.java | 2 + .../APICleanUpTrashOnBackupStorageMsg.java | 2 + .../backup/APIDeleteBackupStorageMsg.java | 2 + ...leteExportedImageFromBackupStorageMsg.java | 2 + .../APIDetachBackupStorageFromZoneMsg.java | 2 + .../APIGetBackupStorageCapacityMsg.java | 2 + .../backup/APIGetBackupStorageTypesMsg.java | 2 + .../backup/APIGetTrashOnBackupStorageMsg.java | 2 + .../backup/APIQueryBackupStorageMsg.java | 2 + .../backup/APIReconnectBackupStorageMsg.java | 2 + .../backup/APIScanBackupStorageMsg.java | 3 + .../backup/APIUpdateBackupStorageMsg.java | 2 + .../primary/APIAddPrimaryStorageMsg.java | 3 + .../primary/APIAddStorageProtocolMsg.java | 2 + .../APIAttachPrimaryStorageToClusterMsg.java | 2 + .../APIChangePrimaryStorageStateMsg.java | 2 + ...ICleanUpImageCacheOnPrimaryStorageMsg.java | 2 + ...leanUpStorageTrashOnPrimaryStorageMsg.java | 2 + .../APICleanUpTrashOnPrimaryStorageMsg.java | 2 + .../primary/APIDeletePrimaryStorageMsg.java | 2 + ...APIDetachPrimaryStorageFromClusterMsg.java | 2 + ...tPrimaryStorageAllocatorStrategiesMsg.java | 2 + .../APIGetPrimaryStorageCapacityMsg.java | 2 + .../APIGetPrimaryStorageLicenseInfoMsg.java | 2 + .../primary/APIGetPrimaryStorageTypesMsg.java | 2 + .../APIGetPrimaryStorageUsageReportMsg.java | 2 + .../APIGetTrashOnPrimaryStorageMsg.java | 2 + ...InstanceMetadataFromPrimaryStorageMsg.java | 2 + .../primary/APIQueryImageCacheMsg.java | 2 + .../primary/APIQueryPrimaryStorageMsg.java | 2 + .../APIReconnectPrimaryStorageMsg.java | 2 + .../primary/APIRegisterVmInstanceMsg.java | 2 + .../APISyncPrimaryStorageCapacityMsg.java | 2 + .../primary/APIUpdatePrimaryStorageMsg.java | 2 + .../snapshot/APIBackupVolumeSnapshotMsg.java | 3 + .../APIBatchDeleteVolumeSnapshotMsg.java | 2 + ...eteVolumeSnapshotFromBackupStorageMsg.java | 2 + .../snapshot/APIGetVolumeSnapshotSizeMsg.java | 2 + .../snapshot/APIQueryVolumeSnapshotMsg.java | 2 + .../APIQueryVolumeSnapshotTreeMsg.java | 2 + .../snapshot/APIShrinkVolumeSnapshotMsg.java | 2 + .../snapshot/APIUpdateVolumeSnapshotMsg.java | 2 + ...PICheckMemorySnapshotGroupConflictMsg.java | 2 + ...eckVolumeSnapshotGroupAvailabilityMsg.java | 2 + .../group/APIQueryVolumeSnapshotGroupMsg.java | 2 + .../APIRevertVmFromSnapshotGroupMsg.java | 2 + .../APIUngroupVolumeSnapshotGroupMsg.java | 2 + .../APIUpdateVolumeSnapshotGroupMsg.java | 2 + .../header/tag/APIAbstractCreateTagMsg.java | 2 + .../header/tag/APICreateSystemTagsMsg.java | 2 + .../header/tag/APIQuerySystemTagMsg.java | 2 + .../zstack/header/tag/APIQueryUserTagMsg.java | 2 + .../vm/APIAttachIsoToVmInstanceMsg.java | 2 + .../header/vm/APIAttachL3NetworkToVmMsg.java | 2 + .../vm/APIAttachL3NetworkToVmNicMsg.java | 2 + .../vm/APIChangeInstanceOfferingMsg.java | 2 + ...CheckVmInstanceMetadataConsistencyMsg.java | 2 + .../vm/APICleanupVmInstanceMetadataMsg.java | 2 + .../vm/APICreateVmInstanceFromVolumeMsg.java | 2 + ...eVmInstanceFromVolumeSnapshotGroupMsg.java | 3 + ...CreateVmInstanceFromVolumeSnapshotMsg.java | 2 + .../header/vm/APICreateVmInstanceMsg.java | 2 + .../zstack/header/vm/APICreateVmNicMsg.java | 2 + .../vm/APIDeleteTemplatedVmInstanceMsg.java | 2 + .../vm/APIDeleteVmConsolePasswordMsg.java | 2 + .../header/vm/APIDeleteVmStaticIpMsg.java | 2 + .../header/vm/APIDestroyVmInstanceMsg.java | 2 + .../vm/APIDetachIsoFromVmInstanceMsg.java | 2 + .../vm/APIDetachL3NetworkFromVmMsg.java | 2 + .../header/vm/APIExpungeVmInstanceMsg.java | 2 + .../header/vm/APIFlattenVmInstanceMsg.java | 2 + .../org/zstack/header/vm/APIFstrimVmMsg.java | 2 + .../APIGetCandidateIsoForAttachingVmMsg.java | 2 + ...ateL3NetworksForChangeVmNicNetworkMsg.java | 2 + ...didatePrimaryStoragesForCreatingVmMsg.java | 3 + .../APIGetCandidateVmForAttachingIsoMsg.java | 2 + ...ateZonesClustersHostsForCreatingVmMsg.java | 2 + ...rdependentL3NetworksBackupStoragesMsg.java | 2 + ...IGetInterdependentL3NetworksImagesMsg.java | 2 + ...APIGetMemorySnapshotGroupReferenceMsg.java | 2 + .../header/vm/APIGetSpiceCertificatesMsg.java | 2 + .../vm/APIGetVmAttachableDataVolumeMsg.java | 2 + .../vm/APIGetVmAttachableL3NetworkMsg.java | 2 + .../header/vm/APIGetVmBootOrderMsg.java | 2 + .../header/vm/APIGetVmCapabilitiesMsg.java | 2 + .../header/vm/APIGetVmConsoleAddressMsg.java | 2 + .../header/vm/APIGetVmConsolePasswordMsg.java | 2 + .../header/vm/APIGetVmDeviceAddressMsg.java | 2 + .../org/zstack/header/vm/APIGetVmDnsMsg.java | 2 + .../zstack/header/vm/APIGetVmHostnameMsg.java | 2 + .../APIGetVmMigrationCandidateHostsMsg.java | 2 + .../APIGetVmNicAttachedNetworkServiceMsg.java | 2 + .../zstack/header/vm/APIGetVmSshKeyMsg.java | 2 + ...etVmStartingCandidateClustersHostsMsg.java | 2 + .../org/zstack/header/vm/APIGetVmTaskMsg.java | 2 + .../zstack/header/vm/APIGetVmUptimeMsg.java | 2 + .../header/vm/APIGetVmsCapabilitiesMsg.java | 2 + .../org/zstack/header/vm/APIMigrateVmMsg.java | 2 + .../header/vm/APIPauseVmInstanceMsg.java | 2 + .../APIPreCheckVmMetadataRegistrationMsg.java | 2 + .../vm/APIQueryTemplatedVmInstanceMsg.java | 2 + .../header/vm/APIQueryVmInstanceMsg.java | 2 + .../zstack/header/vm/APIQueryVmNicMsg.java | 2 + .../vm/APIQueryVmPriorityConfigMsg.java | 2 + .../vm/APIReadVmInstanceMetadataMsg.java | 2 + .../header/vm/APIRebootVmInstanceMsg.java | 2 + .../APIRegisterVmInstanceFromMetadataMsg.java | 2 + .../header/vm/APIResumeVmInstanceMsg.java | 2 + .../vm/APIScanVmInstanceMetadataMsg.java | 2 + .../zstack/header/vm/APISetVmBootModeMsg.java | 2 + .../header/vm/APISetVmBootVolumeMsg.java | 2 + .../header/vm/APISetVmClockTrackMsg.java | 2 + .../header/vm/APISetVmConsolePasswordMsg.java | 2 + .../org/zstack/header/vm/APISetVmDnsMsg.java | 2 + .../zstack/header/vm/APISetVmHostnameMsg.java | 2 + .../header/vm/APISetVmQxlMemoryMsg.java | 2 + .../header/vm/APISetVmSoundTypeMsg.java | 2 + .../zstack/header/vm/APISetVmSshKeyMsg.java | 2 + .../zstack/header/vm/APISetVmStaticIpMsg.java | 2 + .../header/vm/APIStartVmInstanceMsg.java | 2 + .../header/vm/APIStopVmInstanceMsg.java | 2 + .../vm/APITakeVmConsoleScreenshotMsg.java | 2 + .../header/vm/APIUpdatePriorityConfigMsg.java | 2 + .../vm/APIUpdateTemplatedVmInstanceMsg.java | 2 + .../header/vm/APIUpdateVmMetadataMsg.java | 2 + .../header/vm/APIUpdateVmNicDriverMsg.java | 2 + .../header/vm/APIUpdateVmPriorityMsg.java | 2 + .../header/vm/cdrom/APICreateVmCdRomMsg.java | 2 + .../header/vm/cdrom/APIQueryVmCdRomMsg.java | 2 + .../APISetVmInstanceDefaultCdRomMsg.java | 2 + .../header/vm/cdrom/APIUpdateVmCdRomMsg.java | 2 + ...yVmInstanceResourceMetadataArchiveMsg.java | 2 + ...eryVmInstanceResourceMetadataGroupMsg.java | 2 + .../header/vo/APIGetResourceNamesMsg.java | 2 + .../volume/APIAttachDataVolumeToHostMsg.java | 2 + .../header/volume/APIBackupDataVolumeMsg.java | 2 + .../volume/APIBatchSyncVolumeSizeMsg.java | 2 + .../volume/APIChangeVolumeStateMsg.java | 2 + ...CreateDataVolumeFromVolumeSnapshotMsg.java | 2 + ...CreateDataVolumeFromVolumeTemplateMsg.java | 2 + .../header/volume/APICreateDataVolumeMsg.java | 2 + .../volume/APICreateVolumeSnapshotMsg.java | 2 + .../APIDetachDataVolumeFromHostMsg.java | 2 + .../volume/APIExpungeDataVolumeMsg.java | 2 + .../APIGetDataVolumeAttachableVmMsg.java | 2 + .../volume/APIGetVolumeCapabilitiesMsg.java | 2 + .../header/volume/APIGetVolumeFormatMsg.java | 2 + .../header/volume/APIQueryVolumeMsg.java | 2 + .../header/volume/APISyncVolumeSizeMsg.java | 2 + .../volume/APIUndoSnapshotCreationMsg.java | 2 + .../header/volume/APIUpdateVolumeMsg.java | 2 + .../header/zone/APIChangeZoneStateMsg.java | 2 + .../zstack/header/zone/APICreateZoneMsg.java | 2 + .../zstack/header/zone/APIDeleteZoneMsg.java | 2 + .../org/zstack/header/zone/APIGetZoneMsg.java | 2 + .../zstack/header/zone/APIQueryZoneMsg.java | 2 + .../zstack/header/zone/APIUpdateZoneMsg.java | 2 + ...ueryThirdPartyAccountSourceBindingMsg.java | 2 + .../acl/APIAddAccessControlListEntryMsg.java | 134 +-- ...PIAddAccessControlListRedirectRuleMsg.java | 2 + ...hangeAccessControlListRedirectRuleMsg.java | 2 + .../acl/APICreateAccessControlListMsg.java | 140 +-- .../acl/APIDeleteAccessControlListMsg.java | 70 +- .../acl/APIQueryAccessControlListMsg.java | 58 +- .../APIRemoveAccessControlListEntryMsg.java | 110 +- .../appliancevm/APIQueryApplianceVmMsg.java | 2 + .../backup/APIAddCephBackupStorageMsg.java | 2 + .../APIAddMonToCephBackupStorageMsg.java | 2 + .../backup/APIQueryCephBackupStorageMsg.java | 2 + .../APIRemoveMonFromCephBackupStorageMsg.java | 2 + .../APIUpdateCephBackupStorageMonMsg.java | 2 + .../primary/APIAddCephPrimaryStorageMsg.java | 2 + .../APIAddCephPrimaryStoragePoolMsg.java | 2 + .../APIAddMonToCephPrimaryStorageMsg.java | 2 + .../APIDeleteCephPrimaryStoragePoolMsg.java | 2 + .../ceph/primary/APIQueryCephOsdGroupMsg.java | 2 + .../APIQueryCephPrimaryStorageMsg.java | 2 + .../APIQueryCephPrimaryStoragePoolMsg.java | 2 + ...APIRemoveMonFromCephPrimaryStorageMsg.java | 2 + .../APIUpdateCephPrimaryStorageMonMsg.java | 2 + .../APIUpdateCephPrimaryStoragePoolMsg.java | 2 + .../APIAddResourcesToDirectoryMsg.java | 2 + .../directory/APICreateDirectoryMsg.java | 2 + .../directory/APIDeleteDirectoryMsg.java | 2 + .../zstack/directory/APIMoveDirectoryMsg.java | 2 + .../APIMoveResourcesToDirectoryMsg.java | 2 + .../directory/APIQueryDirectoryMsg.java | 2 + .../APIRemoveResourcesFromDirectoryMsg.java | 2 + .../directory/APIUpdateDirectoryMsg.java | 2 + .../network/service/eip/APIAttachEipMsg.java | 2 + .../service/eip/APIChangeEipStateMsg.java | 2 + .../network/service/eip/APICreateEipMsg.java | 2 + .../network/service/eip/APIDeleteEipMsg.java | 2 + .../network/service/eip/APIDetachEipMsg.java | 2 + .../eip/APIGetEipAttachableVmNicsMsg.java | 2 + .../eip/APIGetVmNicAttachableEipsMsg.java | 2 + .../network/service/eip/APIQueryEipMsg.java | 2 + .../network/service/eip/APIUpdateEipMsg.java | 2 + .../APIChangeL3NetworkDhcpIpAddressMsg.java | 2 + .../flat/APIGetL3NetworkDhcpIpAddressMsg.java | 2 + .../flat/APIGetL3NetworkIpStatisticMsg.java | 2 + ...ChangeHostNetworkInterfaceLldpModeMsg.java | 2 + .../APIGetHostNetworkInterfaceLldpMsg.java | 2 + .../APIQueryHostNetworkInterfaceLldpMsg.java | 2 + .../java/org/zstack/kvm/APIAddKVMHostMsg.java | 2 + .../org/zstack/kvm/APIKvmRunShellMsg.java | 2 + .../org/zstack/kvm/APIUpdateKVMHostMsg.java | 2 + .../message/APIQueryHostOsCategoryMsg.java | 2 + .../message/APIQueryKvmHypervisorInfoMsg.java | 2 + .../zstack/ldap/api/APIAddLdapServerMsg.java | 2 + .../ldap/api/APICreateLdapBindingMsg.java | 2 + .../ldap/api/APIDeleteLdapBindingMsg.java | 2 + .../ldap/api/APIDeleteLdapServerMsg.java | 2 + ...APIGetCandidateLdapEntryForBindingMsg.java | 2 + .../zstack/ldap/api/APIGetLdapEntryMsg.java | 2 + .../ldap/api/APIQueryLdapServerMsg.java | 2 + .../api/APISyncAccountsFromLdapServerMsg.java | 2 + .../ldap/api/APIUpdateLdapServerMsg.java | 2 + ...AddAccessControlListToLoadBalancerMsg.java | 188 ++-- .../APIAddBackendServerToServerGroupMsg.java | 2 + ...dCertificateToLoadBalancerListenerMsg.java | 2 + ...dServerGroupToLoadBalancerListenerMsg.java | 2 + .../lb/APIAddVmNicToLoadBalancerMsg.java | 2 + ...ChangeAccessControlListServerGroupMsg.java | 2 + ...APIChangeLoadBalancerBackendServerMsg.java | 2 + .../lb/APIChangeLoadBalancerListenerMsg.java | 2 + .../service/lb/APICreateCertificateMsg.java | 2 + .../lb/APICreateLoadBalancerListenerMsg.java | 2 + .../service/lb/APICreateLoadBalancerMsg.java | 2 + .../APICreateLoadBalancerServerGroupMsg.java | 3 + .../service/lb/APIDeleteCertificateMsg.java | 2 + .../lb/APIDeleteLoadBalancerListenerMsg.java | 2 + .../service/lb/APIDeleteLoadBalancerMsg.java | 2 + .../APIDeleteLoadBalancerServerGroupMsg.java | 2 + ...CandidateL3NetworksForLoadBalancerMsg.java | 100 +- ...tCandidateL3NetworksForServerGroupMsg.java | 2 + ...IGetCandidateVmNicsForLoadBalancerMsg.java | 2 + ...teVmNicsForLoadBalancerServerGroupMsg.java | 2 + ...IGetLoadBalancerListenerACLEntriesMsg.java | 2 + .../service/lb/APIQueryCertificateMsg.java | 2 + .../lb/APIQueryLoadBalancerListenerMsg.java | 2 + .../service/lb/APIQueryLoadBalancerMsg.java | 2 + .../APIQueryLoadBalancerServerGroupMsg.java | 2 + .../service/lb/APIRefreshLoadBalancerMsg.java | 2 + ...eAccessControlListFromLoadBalancerMsg.java | 164 +-- ...RemoveBackendServerFromServerGroupMsg.java | 2 + ...ertificateFromLoadBalancerListenerMsg.java | 2 + ...erverGroupFromLoadBalancerListenerMsg.java | 2 + .../lb/APIRemoveVmNicFromLoadBalancerMsg.java | 2 + .../service/lb/APIUpdateCertificateMsg.java | 2 + .../lb/APIUpdateLoadBalancerListenerMsg.java | 2 + .../service/lb/APIUpdateLoadBalancerMsg.java | 2 + .../APIUpdateLoadBalancerServerGroupMsg.java | 2 + .../local/APIAddLocalPrimaryStorageMsg.java | 2 + ...APIGetLocalStorageHostDiskCapacityMsg.java | 2 + ...calStorageGetVolumeMigratableHostsMsg.java | 2 + .../APIQueryLocalStorageResourceRefMsg.java | 2 + .../nfs/APIAddNfsPrimaryStorageMsg.java | 2 + .../APIAttachPortForwardingRuleMsg.java | 2 + .../APIChangePortForwardingRuleStateMsg.java | 2 + .../APICreatePortForwardingRuleMsg.java | 2 + .../APIDeletePortForwardingRuleMsg.java | 2 + .../APIDetachPortForwardingRuleMsg.java | 2 + ...IGetPortForwardingAttachableVmNicsMsg.java | 2 + .../APIQueryPortForwardingRuleMsg.java | 2 + .../APIUpdatePortForwardingRuleMsg.java | 2 + .../header/APIAddSdnControllerMsg.java | 2 + .../APICreateL2HardwareVxlanNetworkMsg.java | 2 + ...PICreateL2HardwareVxlanNetworkPoolMsg.java | 2 + .../header/APIQuerySdnControllerMsg.java | 2 + .../header/APIRemoveSdnControllerMsg.java | 2 + .../header/APIUpdateSdnControllerMsg.java | 2 + .../APIAddSecurityGroupRuleMsg.java | 2 + .../APIAddVmNicToSecurityGroupMsg.java | 2 + .../APIAttachSecurityGroupToL3NetworkMsg.java | 2 + .../APIChangeSecurityGroupRuleMsg.java | 3 + .../APIChangeSecurityGroupRuleStateMsg.java | 2 + .../APIChangeSecurityGroupStateMsg.java | 2 + .../APIChangeVmNicSecurityPolicyMsg.java | 2 + .../APICreateSecurityGroupMsg.java | 2 + .../APIDeleteSecurityGroupMsg.java | 2 + .../APIDeleteSecurityGroupRuleMsg.java | 2 + .../APIDeleteVmNicFromSecurityGroupMsg.java | 2 + ...PIDetachSecurityGroupFromL3NetworkMsg.java | 2 + ...IGetCandidateVmNicForSecurityGroupMsg.java | 2 + .../APIQuerySecurityGroupMsg.java | 2 + .../APIQuerySecurityGroupRuleMsg.java | 2 + .../APIQueryVmNicInSecurityGroupMsg.java | 2 + .../APIQueryVmNicSecurityPolicyMsg.java | 3 + .../APISetVmNicSecurityGroupMsg.java | 2 + .../APIUpdateSecurityGroupMsg.java | 2 + ...APIUpdateSecurityGroupRulePriorityMsg.java | 3 + .../APIValidateSecurityGroupRuleMsg.java | 2 + .../sftp/APIAddSftpBackupStorageMsg.java | 2 + .../sftp/APIQuerySftpBackupStorageMsg.java | 2 + .../APIReconnectSftpBackupStorageMsg.java | 2 + .../sftp/APIUpdateSftpBackupStorageMsg.java | 2 + ...IAddSharedMountPointPrimaryStorageMsg.java | 2 + .../APIAttachSshKeyPairToVmInstanceMsg.java | 2 + .../sshkeypair/APICreateSshKeyPairMsg.java | 2 + .../sshkeypair/APIDeleteSshKeyPairMsg.java | 2 + .../APIDetachSshKeyPairFromVmInstanceMsg.java | 2 + .../sshkeypair/APIGenerateSshKeyPairMsg.java | 2 + .../sshkeypair/APIQuerySshKeyPairMsg.java | 2 + .../sshkeypair/APIUpdateSshKeyPairMsg.java | 2 + .../header/APICreateL2TfNetworkMsg.java | 2 + .../service/vip/APIChangeVipStateMsg.java | 2 + .../vip/APICheckVipPortAvailabilityMsg.java | 2 + .../network/service/vip/APICreateVipMsg.java | 2 + .../network/service/vip/APIDeleteVipMsg.java | 2 + .../vip/APIGetVipAvailablePortMsg.java | 2 + .../network/service/vip/APIQueryVipMsg.java | 2 + .../network/service/vip/APIUpdateVipMsg.java | 2 + .../APICreateVirtualRouterOfferingMsg.java | 2 + .../APICreateVirtualRouterVmMsg.java | 2 + ...APIGetAttachablePublicL3ForVRouterMsg.java | 2 + .../virtualrouter/APIGetVipUsedPortsMsg.java | 2 + .../APIProvisionVirtualRouterConfigMsg.java | 2 + .../APIQueryVirtualRouterOfferingMsg.java | 2 + .../APIQueryVirtualRouterVmMsg.java | 2 + .../APIReconnectVirtualRouterMsg.java | 2 + .../APIUpdateVirtualRouterMsg.java | 2 + .../APIUpdateVirtualRouterOfferingMsg.java | 2 + .../l2/vxlan/vtep/APICreateVxlanVtepMsg.java | 2 + .../l2/vxlan/vtep/APIQueryVtepMsg.java | 2 + .../APICreateL2VxlanNetworkMsg.java | 2 + .../APIQueryL2VxlanNetworkMsg.java | 2 + .../APICreateL2VxlanNetworkPoolMsg.java | 2 + .../APICreateVniRangeMsg.java | 2 + .../APICreateVxlanPoolRemoteVtepMsg.java | 2 + .../APIDeleteVniRangeMsg.java | 2 + .../APIDeleteVxlanPoolRemoteVtepMsg.java | 2 + .../APIQueryL2VxlanNetworkPoolMsg.java | 2 + .../vxlanNetworkPool/APIQueryVniRangeMsg.java | 2 + .../APIUpdateVniRangeMsg.java | 2 + .../APIGetResourceBindableConfigMsg.java | 2 + .../APIGetResourceConfigMsg.java | 2 + .../APIGetResourceConfigsMsg.java | 2 + .../APIQueryResourceConfigMsg.java | 2 + .../APIUpdateResourceConfigsMsg.java | 2 + .../org/zstack/query/APIBatchQueryMsg.java | 2 + .../java/org/zstack/query/APIZQLQueryMsg.java | 2 + .../search/APIRefreshSearchIndexesMsg.java | 2 + .../simulator/APIAddSimulatorHostMsg.java | 2 + .../APIAddSimulatorBackupStorageMsg.java | 2 + .../APIAddSimulatorPrimaryStorageMsg.java | 2 + 569 files changed, 1598 insertions(+), 7532 deletions(-) delete mode 100644 "docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" delete mode 100644 "docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" delete mode 100644 "docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" delete mode 100644 "docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" delete mode 100644 "docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" delete mode 100644 "docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" delete mode 100644 "docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" delete mode 100644 "docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" delete mode 100644 "docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" delete mode 100644 "docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" delete mode 100644 "docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" delete mode 100644 "docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" delete mode 100644 "docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" delete mode 100644 "docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" delete mode 100644 "docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" delete mode 100644 "docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" delete mode 100644 "docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" diff --git a/core/src/main/java/org/zstack/core/captcha/APIRefreshCaptchaMsg.java b/core/src/main/java/org/zstack/core/captcha/APIRefreshCaptchaMsg.java index d2e065ee762..8119aa90fbb 100644 --- a/core/src/main/java/org/zstack/core/captcha/APIRefreshCaptchaMsg.java +++ b/core/src/main/java/org/zstack/core/captcha/APIRefreshCaptchaMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by kayo on 2018/7/6. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIRefreshCaptchaReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRefreshCaptchaMsg extends APISyncCallMessage { @APIParam(resourceType = CaptchaVO.class) private String uuid; diff --git a/core/src/main/java/org/zstack/core/config/APIGetGlobalConfigOptionsMsg.java b/core/src/main/java/org/zstack/core/config/APIGetGlobalConfigOptionsMsg.java index 147bbae5ea8..1f832eec732 100644 --- a/core/src/main/java/org/zstack/core/config/APIGetGlobalConfigOptionsMsg.java +++ b/core/src/main/java/org/zstack/core/config/APIGetGlobalConfigOptionsMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/global-configurations/{category}/{name}", method = HttpMethod.GET, responseClass = APIGetGlobalConfigOptionsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetGlobalConfigOptionsMsg extends APISyncCallMessage { @APIParam private String category; diff --git a/core/src/main/java/org/zstack/core/config/APIQueryGlobalConfigMsg.java b/core/src/main/java/org/zstack/core/config/APIQueryGlobalConfigMsg.java index 0a4d1f2f05e..e2ced0d00d1 100755 --- a/core/src/main/java/org/zstack/core/config/APIQueryGlobalConfigMsg.java +++ b/core/src/main/java/org/zstack/core/config/APIQueryGlobalConfigMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryGlobalConfigReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryGlobalConfigMsg extends APIQueryMessage { public static List __example__() { return asList(); diff --git a/core/src/main/java/org/zstack/core/config/APIResetGlobalConfigMsg.java b/core/src/main/java/org/zstack/core/config/APIResetGlobalConfigMsg.java index 1e1bb6218ff..3ba86dda65a 100644 --- a/core/src/main/java/org/zstack/core/config/APIResetGlobalConfigMsg.java +++ b/core/src/main/java/org/zstack/core/config/APIResetGlobalConfigMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIEvent; import org.zstack.header.message.APIMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/global-configurations/actions", @@ -11,6 +12,7 @@ isAction = true, responseClass = APIResetGlobalConfigEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIResetGlobalConfigMsg extends APIMessage { public static APIResetGlobalConfigMsg __example__() { APIResetGlobalConfigMsg msg = new APIResetGlobalConfigMsg(); diff --git a/core/src/main/java/org/zstack/core/config/APIUpdateGlobalConfigMsg.java b/core/src/main/java/org/zstack/core/config/APIUpdateGlobalConfigMsg.java index 60d9a84c744..15642e05475 100755 --- a/core/src/main/java/org/zstack/core/config/APIUpdateGlobalConfigMsg.java +++ b/core/src/main/java/org/zstack/core/config/APIUpdateGlobalConfigMsg.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.MetadataImpact; @RestRequest( path = "/global-configurations/{category}/{name}/actions", @@ -12,6 +13,7 @@ isAction = true, responseClass = APIUpdateGlobalConfigEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateGlobalConfigMsg extends APIMessage { @APIParam private String category; diff --git a/core/src/main/java/org/zstack/core/debug/APICleanQueueMsg.java b/core/src/main/java/org/zstack/core/debug/APICleanQueueMsg.java index 8c7b9472e38..4ae02f72063 100644 --- a/core/src/main/java/org/zstack/core/debug/APICleanQueueMsg.java +++ b/core/src/main/java/org/zstack/core/debug/APICleanQueueMsg.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.MetadataImpact; /** * Created by LiangHanYu on 2021/5/20 14:51 @@ -14,6 +15,7 @@ isAction = true, method = HttpMethod.PUT, responseClass = APICleanQueueEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanQueueMsg extends APIMessage { @APIParam() private String signatureName; diff --git a/core/src/main/java/org/zstack/core/debug/APIDebugSignalMsg.java b/core/src/main/java/org/zstack/core/debug/APIDebugSignalMsg.java index 9bdedf9e75a..92d402b07f3 100755 --- a/core/src/main/java/org/zstack/core/debug/APIDebugSignalMsg.java +++ b/core/src/main/java/org/zstack/core/debug/APIDebugSignalMsg.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/7/25. @@ -17,6 +18,7 @@ parameterName = "params", responseClass = APIDebugSignalEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDebugSignalMsg extends APIMessage { @APIParam private List signals; diff --git a/core/src/main/java/org/zstack/core/debug/APIGetDebugSignalMsg.java b/core/src/main/java/org/zstack/core/debug/APIGetDebugSignalMsg.java index bb167dde056..e32728a1078 100644 --- a/core/src/main/java/org/zstack/core/debug/APIGetDebugSignalMsg.java +++ b/core/src/main/java/org/zstack/core/debug/APIGetDebugSignalMsg.java @@ -3,11 +3,13 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/debug", method = HttpMethod.GET, responseClass = APIGetDebugSignalReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetDebugSignalMsg extends APISyncCallMessage { public static APIGetDebugSignalMsg __example__() { APIGetDebugSignalMsg msg = new APIGetDebugSignalMsg(); diff --git a/core/src/main/java/org/zstack/core/errorcode/APICheckElaborationContentMsg.java b/core/src/main/java/org/zstack/core/errorcode/APICheckElaborationContentMsg.java index cfbd9139bbe..0fde4e1ce4b 100644 --- a/core/src/main/java/org/zstack/core/errorcode/APICheckElaborationContentMsg.java +++ b/core/src/main/java/org/zstack/core/errorcode/APICheckElaborationContentMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/21. @@ -14,6 +15,7 @@ responseClass = APICheckElaborationContentReply.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckElaborationContentMsg extends APISyncCallMessage { @APIParam(nonempty = true, emptyString = false, required = false) private String elaborateFile; diff --git a/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationCategoriesMsg.java b/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationCategoriesMsg.java index bd21a31fd98..d236690e8e0 100644 --- a/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationCategoriesMsg.java +++ b/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationCategoriesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/1. @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetElaborationCategoriesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetElaborationCategoriesMsg extends APISyncCallMessage { public static APIGetElaborationCategoriesMsg __example__() { return new APIGetElaborationCategoriesMsg(); diff --git a/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationsMsg.java b/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationsMsg.java index a4cc9a4ef9a..356af26bc61 100644 --- a/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationsMsg.java +++ b/core/src/main/java/org/zstack/core/errorcode/APIGetElaborationsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/1. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetElaborationsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetElaborationsMsg extends APISyncCallMessage { @APIParam(required = false) private String category; diff --git a/core/src/main/java/org/zstack/core/errorcode/APIReloadElaborationMsg.java b/core/src/main/java/org/zstack/core/errorcode/APIReloadElaborationMsg.java index 33335fce7be..8333cdc992b 100644 --- a/core/src/main/java/org/zstack/core/errorcode/APIReloadElaborationMsg.java +++ b/core/src/main/java/org/zstack/core/errorcode/APIReloadElaborationMsg.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.MetadataImpact; /** * Created by mingjian.deng on 2018/12/1. @@ -13,6 +14,7 @@ responseClass = APIReloadElaborationEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReloadElaborationMsg extends APIMessage { public static APIReloadElaborationMsg __example__() { diff --git a/core/src/main/java/org/zstack/core/eventlog/APIQueryEventLogMsg.java b/core/src/main/java/org/zstack/core/eventlog/APIQueryEventLogMsg.java index 9f8d4dbf227..c34bf9f8469 100644 --- a/core/src/main/java/org/zstack/core/eventlog/APIQueryEventLogMsg.java +++ b/core/src/main/java/org/zstack/core/eventlog/APIQueryEventLogMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/eventlogs", @@ -16,6 +17,7 @@ responseClass = APIQueryEventLogReply.class ) @AutoQuery(replyClass = APIQueryEventLogReply.class, inventoryClass = EventLogInventory.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryEventLogMsg extends APIQueryMessage { public static List __example__() { return asList(); diff --git a/core/src/main/java/org/zstack/core/gc/APIDeleteGCJobMsg.java b/core/src/main/java/org/zstack/core/gc/APIDeleteGCJobMsg.java index 6393e702a3b..88240caeb5e 100755 --- a/core/src/main/java/org/zstack/core/gc/APIDeleteGCJobMsg.java +++ b/core/src/main/java/org/zstack/core/gc/APIDeleteGCJobMsg.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.MetadataImpact; /** * Created by xing5 on 2017/3/5. @@ -14,6 +15,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteGCJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteGCJobMsg extends APIMessage implements GarbageCollectorMessage { @APIParam private String uuid; diff --git a/core/src/main/java/org/zstack/core/gc/APIQueryGCJobMsg.java b/core/src/main/java/org/zstack/core/gc/APIQueryGCJobMsg.java index 821be04bc3d..82d894fabe2 100755 --- a/core/src/main/java/org/zstack/core/gc/APIQueryGCJobMsg.java +++ b/core/src/main/java/org/zstack/core/gc/APIQueryGCJobMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/3/5. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryGCJobReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryGCJobMsg extends APIQueryMessage { public static List __example__() { return asList("name=gc", "state=Enabled"); diff --git a/core/src/main/java/org/zstack/core/gc/APITriggerGCJobMsg.java b/core/src/main/java/org/zstack/core/gc/APITriggerGCJobMsg.java index 5555a9c33ec..a2f30af6622 100755 --- a/core/src/main/java/org/zstack/core/gc/APITriggerGCJobMsg.java +++ b/core/src/main/java/org/zstack/core/gc/APITriggerGCJobMsg.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.MetadataImpact; /** * Created by xing5 on 2017/3/5. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APITriggerGCJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APITriggerGCJobMsg extends APIMessage implements GarbageCollectorMessage { @APIParam(resourceType = GarbageCollectorVO.class) private String uuid; diff --git "a/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" "b/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" deleted file mode 100644 index 1a343a0dd96..00000000000 --- "a/docs/design/vm-metadata-00-\350\256\276\350\256\241\347\272\246\346\235\237\347\264\242\345\274\225.md" +++ /dev/null @@ -1,94 +0,0 @@ -# VM 元数据 — 设计约束索引 - -> 本文档汇总所有 vm-metadata 设计文档中的约束条目(C-*),提供跨文档查找的统一入口。 -> 每条约束标注 ID、简要描述和来源文档/章节。 - -## Part 1b — 拦截器(vm-metadata-01b-API拦截与VM解析.md) - -| ID | 约束 | 来源 | -|----|------|------| -| C-IC | `INTERNAL_METADATA_MESSAGES` 与 handler `markDirty()` 调用点一一可追溯 | §5 | -| C-IM | 所有 `APIMessage` 子类必须标注 `@MetadataImpact`(可为 NONE),CI 扫描全量子类 | §5 | -| C-PA | `pendingApis` 超时清理 + afterCompletion null-safe + 清理时补 markDirty | §5 | -| C-RS | Resolver 选择匹配 API 资源语义;删除/卸载类 API 使用 pre-capture | §5 | -| C-H1 | STORAGE 级内部消息 handler 必须调用 `markDirty()`;CI ERROR 阻断 | §5 | -| C-M4 | `pendingApis` 超时通过 GlobalConfig 配置,不得硬编码 | §5 | - -## Part 1c — 存储层(vm-metadata-01c-存储层与模板虚拟机.md) - -| ID | 约束 | 来源 | -|----|------|------| -| C-01C-2 | sblk LV 名称 `{vm_uuid}_vmmeta`,长度 39,< LVM 128 上限 | §4 | -| C-01C-3 | 模板 VM 元数据锚定 RootVolume 所在 PS | §4 | -| C-01C-4 | 存储迁移:目标端同步写入 + read-back 校验后才能清理源端 | §4 | -| C-01C-5 | 清理时校验根盘 `primaryStorageUuid` 仍在源 PS | §4 | -| C-01C-6 | flush 路径动态解析,不缓存历史路径 | §4 | -| C-01C-7 | 迁移 `nextRetryTime` 暂停/恢复成对;失败回滚恢复 Poller | §4 | -| C-01C-8 | MN 启动时重置 `nextRetryTime='2099-...'` 的暂停行(Poller 启动前) | §4 | -| C-01C-9 | `deleteMetadata` 幂等(不存在不抛异常) | §4 | -| C-01C-10 | local/NFS tmp 文件固定命名,Agent 启动时清理残留 | §4 | -| C-01C-11 | `MetadataStorageHandler` 包含 `scanMetadataVmUuids()`(Q15) | §4 | -| C-01C-12 | `deleteMetadata` 重试参数通过 GlobalConfig 配置(Q12) | §4 | - -## Part 2 — Dirty Mark & Poller(vm-metadata-02-脏标记与Poller.md) - -| ID | 约束 | 来源 | -|----|------|------| -| C-DM-01 | `markDirty` 使用 `INSERT IGNORE + UPDATE` 两步,禁止 `ON DUPLICATE KEY`(Q19) | §7 | -| C-CL-02 | claim 成功必须写入 `lastClaimTime`;僵尸清理 15 分钟(独立任务 DP-05) | §7 | -| C-TM-03 | `doFlush` 超时 ≥ 5 分钟,超时进入 `onFlushFailure` | §7 | -| C-RB-04 | 指数退避参数来自 GlobalConfig(baseDelay/maxExponent) | §7 | -| C-SR-05 | 重试耗尽必须标记 `lastFlushFailed=true`,不得静默放弃 | §7 | -| C-SR-06 | StaleRecoveryTask 的 markDirty 使用 retryCount=0,验证返回值后才清除 lastFlushFailed(DP-03) | §7 | -| C-SC-07 | `storageStructureChange` 仅在存储拓扑操作时设置;升级场景始终 true | §7 | -| C-FL-08 | `doFlush` 过滤 Destroyed VM dirty 行,主动删除释放 Poller(Q34) | §7 | -| C-TF-09 | `triggerFlushForVm` stale claim 接管阈值通过 `vm.metadata.triggerFlush.staleMinutes` 配置(默认 10 min),不得与 `staleClaim.thresholdMinutes`(30 min 后台扫描)混淆 | §7, DP-06 | - -## Part 2b — HA & 运维(vm-metadata-02b-高可用与运维.md) - -| ID | 约束 | 来源 | -|----|------|------| -| C-02B-1 | `nodeLeft()` 延迟 5s 后触发 `claimAndFlush()`,不立即抢占 | §15 | -| C-02B-2 | sblk 写入前校验 `managementNodeUuid == 本 MN`(Fence Check) | §15 | -| C-02B-3 | 路径巡检禁止 `listAll`,必须 keyset 分页 | §15 | -| C-02B-4 | 升级刷新分批执行(默认 1000),避免单次超大事务 | §15 | -| C-02B-5 | payload 上限:静态 30MB + 运行时 slot 容量校验 | §15 | -| C-02B-6 | `storageStructureChange` OR 语义,dirty 行删除前不降级 | §15 | -| C-02B-7 | 容量常量集中定义,禁止硬编码散落 | §15 | -| C-02B-8 | `lastFlushFailed` 仅 retry 耗尽时 true,仅 StaleRecoveryTask 重置 false | §15 | -| C-02B-9 | 升级刷新前检查 15 分钟内无 nodeLeft(M3) | §15 | -| C-02B-10 | nodeLeft 延迟调整需与 Fence Check 配合评估 | §15 | -| C-02B-11 | `false→true` 初始化分批 + 批间延迟 | §15 | -| C-02B-12 | Cleanup API 仅 enabled=false 时允许 | §15 | -| C-02B-13 | 初始化每批重检 enabled 开关 | §15 | -| C-02B-14 | 孤儿检测仅报告不自动删除 | §15 | - -## Part 3 — 注册(vm-metadata-03-注册与运维.md) - -| ID | 约束 | 来源 | -|----|------|------| -| C-03-1 | `parentId` 注册时统一置 null | §9 | -| C-03-2 | 跨存储拒绝注册,返回 expected/actual PS UUID | §9 | -| C-03-3 | installPath 前缀替换满足分隔符边界 | §9 | -| C-03-4 | 回滚"由外到内"+ 空树清理 SQL | §9 | -| C-03-5 | ChainTask 超时 35 分钟;LongJob cancel 触发 rollback | §9 | -| C-03-6 | Root Volume path 缺失 BLOCK;Data Volume WARN | §9 | -| C-03-7 | 注册成功后触发 ConsistencyCheck | §9 | -| C-03-8 | PreCheck 与 Register 共享校验方法 | §9 | - ---- - -**总计**:6(Part 1b)+ 11(Part 1c)+ 8(Part 2)+ 14(Part 2b)+ 8(Part 3)= **47 条约束** - ---- - -## Part 7 — 测试计划 - -测试计划分为 4 个文档,约 190+ 条用例,按约束 ID 交叉引用: - -| 文档 | 范围 | 用例前缀 | -|------|------|----------| -| [Part 7a — 单元测试](vm-metadata-07a-单元测试计划.md) | 序列化 Round-Trip、DTO 构建、路径指纹、markDirty 逻辑、注解覆盖率、Resolver 链、容量计算、sblk 编解码、注册字段映射、installPath 替换 | UT-* | -| [Part 7b — 集成测试](vm-metadata-07b-集成测试计划.md) | sblk 写入读取、local/NFS JSON 读写、Poller 端到端、API 拦截器联动、存储迁移链路、注册端到端、路径巡检、API 端到端 | IT-* | -| [Part 7c — 故障注入](vm-metadata-07c-故障注入测试.md) | sblk 三阶段崩溃恢复、MN 重启清理、双 MN 故障转移、DB 异常、Agent 异常、功能开关竞态 | FI-* | -| [Part 7d — 性能与补充](vm-metadata-07d-性能与补充测试.md) | 1000/10000 VM 全量更新基准、升级批次压力、注册耗时、Poller 吞吐、E2E 场景、兼容性、安全权限、可观测性、GlobalConfig 动态生效 | PERF-*/E2E-*/COMPAT-*/SEC-*/OBS-*/CFG-* | diff --git "a/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" "b/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" deleted file mode 100644 index 3ed69abdd49..00000000000 --- "a/docs/design/vm-metadata-01a-\346\225\260\346\215\256\346\250\241\345\236\213\344\270\216\345\272\217\345\210\227\345\214\226.md" +++ /dev/null @@ -1,417 +0,0 @@ -# VM 元数据 — 数据模型与序列化 - -## 目录 - -1. [概述](#1-概述) -2. [核心 DTO 结构](#2-核心-dto-结构) -3. [编码策略 — per-Resource 字段级 Base64](#3-编码策略--per-resource-字段级-base64) -4. [序列化关注点](#4-序列化关注点) -5. [反序列化关注点](#5-反序列化关注点) -6. [schemaVersion 版本规则](#6-schemaversion-版本规则) -7. [VmCdRomVO 等附属资源](#7-vmcdromvo-等附属资源) - ---- - -## 1. 概述 - -虚拟机元数据功能用于将 VM 及其关联资源(云盘、网卡、快照、SystemTag、ResourceConfig)的关键信息持久化到主存储上,以支持跨平台/灾难恢复场景下的虚拟机注册恢复。 - -### 1.1 适用范围 - -本功能仅适用于 **`type = "UserVm"`** 的虚拟机实例。ApplianceVm(虚拟路由器、网关等系统 VM)不写入元数据、不支持注册。 - -`@MetadataImpact` 拦截器和 `buildVmInstanceMetadata()` 中均增加 `vmInstanceType != "UserVm"` 前置检查,不满足时静默跳过。 - -### 1.2 全局配置 - -增加全局配置项 `vm.metadata.enabled`(Boolean,**默认为 false**),开启/关闭记录虚拟机元数据。 - -**理由**:注册虚拟机仅用于有容灾需求的场景。对于 99.9% 普通用户来说不会用到此功能。 - -#### 1.2.1 开关切换策略 - -| 切换方向 | 行为 | 说明 | -|----------|------|------| -| **`false → true`** | 触发一次**分批全量 markDirty** 初始化所有 VM 的元数据 | 防止读写风暴,复用 Poller 自动限流 | -| **`true → false`** | **不自动删除**已有元数据文件/LV | 提供 `APICleanupVmInstanceMetadataMsg` 按需清理 | - -**`false → true`(启用)详细流程**: - -通过 `GlobalConfig.installUpdateExtension` 监听 `vm.metadata.enabled` 变更。检测到从 `false` 变为 `true` 时,提交延迟 30 秒的初始化任务(等待 Poller 启动就绪),执行分批 markDirty: - -```java -VmGlobalConfig.VM_METADATA_ENABLED.installUpdateExtension((oldValue, newValue) -> { - boolean wasEnabled = Boolean.parseBoolean(oldValue); - boolean nowEnabled = Boolean.parseBoolean(newValue); - - if (!wasEnabled && nowEnabled) { - // false → true: 分批初始化全量 VM 元数据 - submitBatchInitialization(); - } - // true → false: 不做任何自动操作 -}); -``` - -`submitBatchInitialization()` 逻辑与升级全量刷新([Part 2b §9.2](vm-metadata-02b-高可用与运维.md#92-刷新执行简化无-longjob))共用相同的分批 SQL 模式,但使用独立配置项控制批次大小和批间延迟(详见 [Part 2b §9a](vm-metadata-02b-高可用与运维.md#9a-功能开关切换处理))。 - -**`true → false`(禁用)详细说明**: - -- Poller 的 `markDirty()` 和 `triggerFlushForVm()` 内前置检查 `vm.metadata.enabled`,关闭后自动停止新的标脏和刷写 -- 已存在的 dirty 行不主动清理(自然过期或下次启用时重新处理) -- 已写入存储的元数据文件/LV **保留不删除**,避免误操作导致已有元数据丢失 -- 运维可通过 `APICleanupVmInstanceMetadataMsg`(见 [Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据))按需批量清理指定 PS 或指定 VM 的元数据 - -### 1.3 安全声明 - -- USERDATA 和 SystemTag 中可能包含 cloud-init 脚本 或者 password 信息。 -- 未来如需加密,可通过 bump HeaderVersion 引入 `EncryptionType` 字段扩展。 - -### 1.4 命名规范 - -| 层面 | 前缀 | 示例 | -|------|------|------| -| DTO(对应 VmInstanceVO) | `VmInstanceMetadata*` | `VmInstanceMetadataDTO`、`VmInstanceMetadataCodec` | -| VO / DB 表 / 内部组件 | `VmMetadata*` | `VmMetadataDirtyVO`、`VmMetadataPathFingerprintVO`、`MetadataDirtyPoller` | - -### 1.5 构建事务性能监控 - -`buildVmInstanceMetadata()` 运行在 `REPEATABLE READ` 事务中以保障快照一致性。: - -- 处置建议:暂时不用考虑,可预期范围内,一个虚拟机没有那么多盘和快照,查询会很快。且元数据更新保证最终一致性。 - ---- - -## 2. 核心 DTO 结构 - -### 2.1 VmInstanceMetadataDTO - -``` -VmInstanceMetadataDTO -├── schemaVersion: String // 元数据 schema 版本(与 zsv 数据库版本一致) -├── vmCategory: VmMetadataCategory // VM 类型(REGULAR / TEMPLATE / TEMPLATE_CACHE) -├── vm: ResourceMetadata // 虚拟机自身 -├── volumes: List // 根盘 + 数据盘(含引用) -├── nics: List // 网卡(仅记录,注册时不恢复) -├── snapshots: List // 全部卷的 VolumeSnapshotVO JSON 列表(VM 级别扁平化) -├── snapshotGroups: List // List(VM 级别,横跨多卷) -└── snapshotGroupRefs: List // List(VM 级别) -``` - -**前置约束**: -- VM 必须有 Root Volume。无 Root Volume 的 VM(非法状态)跳过元数据构建,`markDirty()` 时以 WARN 日志记录。 -- `volumes` 列表仅包含 `isShareable=false` 的 Volume。共享盘(`isShareable=true`)不纳入元数据,注册时也不恢复。理由:共享盘可能同时挂载在多个 VM 上,跨平台恢复时无法保证共享语义的一致性。 -- **共享盘快照排除(讨论澄清)**:构建 `snapshots` 列表时,查询条件应排除共享盘的快照(`WHERE volumeUuid IN (非共享盘 UUID 列表)`)。若 VM 的数据盘中有共享盘,其快照也不纳入元数据。 -- **空快照列表防护(讨论澄清)**:若 VM 的所有卷均无快照(`allSnapshots.isEmpty()`),跳过 `VolumeSnapshotTree.fromVOs()` 调用,直接设置 `dto.snapshots = Collections.emptyList()`。避免向空输入传递导致潜在的 NPE。 - -**确定性排序规则**:DB 数据不变时,多次构建必须产出完全相同的 JSON。所有 List 字段在序列化前按主键升序排列: - -| 字段 | 排序键 | -|------|--------| -| `volumes` | `VolumeVO.uuid` | -| `nics` | `VmNicVO.uuid` | -| `snapshots` | **BFS 拓扑排序**(见下方说明) | -| `snapshotGroups` | `VolumeSnapshotGroupVO.uuid` | -| `snapshotGroupRefs` | `volumeSnapshotGroupUuid` 优先,`volumeUuid` 次之(复合键字典序) | -| `VolumeResourceMetadata.snapshotReferences` | `VolumeSnapshotReferenceVO.id` | -| `VolumeResourceMetadata.snapshotReferenceTrees` | `VolumeSnapshotReferenceTreeVO.uuid` | -| `systemTags`(Base64 编码前) | `SystemTagVO.uuid` | -| `resourceConfigs`(Base64 编码前) | `ResourceConfigVO.uuid` | - -**snapshots 拓扑排序规则**(保证父快照在子快照之前,支持注册时按序恢复): - -``` -1. 按 volumeUuid 分组,每组再按 treeUuid 分组 -2. 同一 tree 内:使用已有的 VolumeSnapshotTree.fromVOs() + levelOrderTraversal() - - 根节点(parentUuid = null)排最前 - - BFS 层序遍历,父先于子 -3. 不同 tree 之间按 treeUuid ASC 排列 -4. 不同 volume 之间按 volumeUuid ASC 排列 -``` - -保证:**父先于子**(BFS 天然保证)、**确定性**(同层顺序由 `VolumeSnapshotTree` 内部保证)、**稳定性**(纯粹由 uuid + parentUuid 决定)。 - -**循环引用防护**:`VolumeSnapshotTree.fromVOs()` 内部以 `parentUuid` 构建有向图。若数据库中快照链存在循环引用(如 A→B→C→A,属于数据库层面的非法数据),BFS 遍历不会访问环上节点(无入度为 0 的根节点可达这些节点)。这些"孤立环"节点将不会出现在 `levelOrderTraversal()` 输出中。处理策略:构建完成后比对输出数量与输入数量——若 `result.size() < allSnapshots.size()`,说明存在不可达快照(环或孤立节点),记录 WARN 日志 `"Unreachable snapshots detected for VM {vmUuid}: {count} out of {total}, possible circular reference"` 并将遗漏的快照按 uuid ASC 追加到结果尾部(保证不丢数据,注册时依赖 FK 而非顺序重建关系)。 - -**排序安全性说明**:BFS 拓扑排序仅改变输出顺序,不改变节点内容。`parentUuid/parentId` 关系通过快照 VO 字段完整保留,注册恢复时不会发生层级信息丢失。 - -> **复用已有基础设施**:`VolumeSnapshotTree`([VolumeSnapshotTree.java](../header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotTree.java))已有完整的树构建和 BFS 层序遍历实现,无需重新实现。 - -```java -// 实际实现:复用 VolumeSnapshotTree -List topoSort(List allSnapshots) { - // 先按 volumeUuid 分组,再按 treeUuid 分组(双层 TreeMap 保证 ASC 排序) - Map>> byVolumeThenTree = - allSnapshots.stream().collect(Collectors.groupingBy( - VolumeSnapshotVO::getVolumeUuid, TreeMap::new, - Collectors.groupingBy(VolumeSnapshotVO::getTreeUuid, - TreeMap::new, Collectors.toList()))); - - List result = new ArrayList<>(); - for (Map> treesInVolume : byVolumeThenTree.values()) { - for (List treeSnapshots : treesInVolume.values()) { - VolumeSnapshotTree tree = VolumeSnapshotTree.fromVOs(treeSnapshots); - List ordered = tree.levelOrderTraversal(); - for (VolumeSnapshotInventory inv : ordered) { - result.add(findByUuid(treeSnapshots, inv.getUuid())); - } - } - } - return result; -} -``` - -### 2.2 VmMetadataCategory 枚举 - -> **权威定义**:此枚举同时适用于 Java DTO 和 sblk Header VM 摘要区。Part 4b Header 中 `VmCategory` 字段的取值与此枚举一一对应。 - -```java -public enum VmMetadataCategory { - REGULAR, // 0 — 普通虚拟机(含链式克隆子 VM) - TEMPLATE, // 1 — 模板虚拟机(TemplatedVmInstanceVO 存在) - TEMPLATE_CACHE // 2 — 模板缓存虚拟机(TemplatedVmInstanceCacheVO 中的 cacheVmInstanceUuid) -} -``` - -| 类型 | 判定条件 | 写入元数据 | 注册行为 | -|------|----------|:---:|----------| -| `REGULAR` | 非模板、非缓存的所有 VM | (Y) | 正常注册 | -| `TEMPLATE` | `TemplatedVmInstanceVO` 存在 | (Y) | 注册为普通 VM(不恢复模板身份) | -| `TEMPLATE_CACHE` | `TemplatedVmInstanceCacheVO.cacheVmInstanceUuid` 匹配 | (Y) | **拒绝注册**(返回 `METADATA_CACHE_VM_NOT_REGISTERABLE`) | - -**扩展约束**:枚举在存储/传输层按 **int** 语义处理,当前占用值 `0~2`,预留 `3~99` 给未来类别扩展,避免与历史版本冲突。 - -**向后兼容策略**:新增枚举值时(如 v2+ 加入 `APPLIANCE = 3`),旧版本 Agent 读取到未知 int 值应按 `REGULAR` 降级处理(安全默认值)。Java 端 Gson 反序列化遇到未知枚举值返回 null,代码中对 `vmCategory == null` 已统一视为 `REGULAR`。因此无需额外版本协商,仅需在新增枚举时更新此注释标注已占用值。 - -**构建时判定逻辑**(先判缓存再判模板): - -```java -if (Q.New(TemplatedVmInstanceCacheVO.class) - .eq(TemplatedVmInstanceCacheVO_.cacheVmInstanceUuid, vmUuid) - .isExists()) { - dto.vmCategory = VmMetadataCategory.TEMPLATE_CACHE; -} else if (Q.New(TemplatedVmInstanceVO.class) - .eq(TemplatedVmInstanceVO_.uuid, vmUuid) - .isExists()) { - dto.vmCategory = VmMetadataCategory.TEMPLATE; -} else { - dto.vmCategory = VmMetadataCategory.REGULAR; -} -``` - -### 2.3 ResourceMetadata - -``` -ResourceMetadata -├── resourceUuid: String // 资源 UUID(冗余,必须与 vo 内部 uuid 一致) -├── vo: String // VO 全量 JSON 明文 -├── systemTags: String // 白名单过滤后的 SystemTagVO JSON 列表 Base64 编码 -└── resourceConfigs: String // 白名单过滤后的 ResourceConfigVO JSON 列表 Base64 编码 -``` - -### 2.4 VolumeResourceMetadata - -`VolumeResourceMetadata` 继承 `ResourceMetadata`,额外携带该卷的引用数据: - -``` -VolumeResourceMetadata extends ResourceMetadata -├── (inherited: resourceUuid, vo, systemTags, resourceConfigs) -├── snapshotReferences: List // 该卷的 VolumeSnapshotReferenceVO JSON 列表 -└── snapshotReferenceTrees: List // 该卷关联的 VolumeSnapshotReferenceTreeVO JSON 列表 -``` - -> **快照数据归属**:快照(`VolumeSnapshotVO`)提升到 `VmInstanceMetadataDTO.snapshots` 扁平列表,与 `snapshotGroups`/`snapshotGroupRefs` 同级。 -> 引用数据(`snapshotReferences`/`snapshotReferenceTrees`)仍保留在 `VolumeResourceMetadata` 内,因为引用关系与具体卷紧密绑定。 -> 注册时 ReferenceVO 的全局拓扑排序(按 parentId 依赖)从各 volume 收集后统一处理。 - -**快照提升到 DTO 层的设计理由**: - -| # | 好处 | 详细说明 | -|---|------|----------| -| 1 | **注册恢复零额外操作** | 注册时先 persist 所有 VolumeVO → 再遍历 `snapshots` 列表逐条 persist VolumeSnapshotVO,`volumeUuid` FK 天然满足。若放在 Volume 层,需先解包每个 Volume 的 snapshots 再逐卷恢复,多一层循环嵌套。 | -| 2 | **与 snapshotGroups 同构** | `snapshotGroups`(VolumeSnapshotGroupVO)是天然 VM 级别概念(一个 group 横跨多卷)。snapshots 放同级后,三个快照相关列表在同一层级统一管理,结构对称。 | -| 3 | **全局拓扑排序一次完成** | 快照链变基(sblk rebase)需要全局拓扑顺序。扁平列表直接做一次 BFS 拓扑排序即可,无需先合并再排序。 | -| 4 | **一致性检查简化** | 对比 DB 与存储上的快照时,直接比对两个扁平列表。无需逐 Volume 打开再逐一比对。 | -| 5 | **Payload 构建效率** | Builder 一次 `SELECT * FROM VolumeSnapshotVO WHERE volumeUuid IN (...)` 查出所有快照,序列化为一个列表。 | -| 6 | **VolumeResourceMetadata 保持精简** | Volume 层只保留与本卷强绑定的数据(ReferenceVO/ReferenceTreeVO)。快照通过 `volumeUuid` 字段自带归属,无需冗余嵌套。 | - -> **代价**:失去 Volume→Snapshot 的直观嵌套结构。但每条 `VolumeSnapshotVO` 自带 `volumeUuid`,注册时 `groupBy(volumeUuid)` 即可按卷分组,O(N) 遍历,可忽略。 - -| VO | 含义 | 注册意义 | -|----|------|----------| -| `VolumeSnapshotReferenceVO` | 记录链式克隆时子 VM 卷对缓存 VM 快照的依赖关系 | 不恢复会导致快照引用计数为 0,子 VM 执行 flatten/删除时无法正确清理物理快照文件 | -| `VolumeSnapshotReferenceTreeVO` | 引用树根节点,记录底层快照链的根信息 | 完全独立表(零 FK 约束),维护引用链路的树形结构 | - -**VolumeSnapshotReferenceVO 查询范围说明(讨论澄清)**:构建 `VolumeResourceMetadata.snapshotReferences` 时,查询条件为 `WHERE referenceVolumeUuid = 当前 VM 的卷 UUID`(即子 VM 自身的卷 UUID),而非 `volumeUuid`(缓存 VM 的卷)。原因:一个 VM 只关心自己的引用记录,不需要包含其他 VM 对同一缓存快照的引用。`referenceVolumeUuid` 是 FK → `VolumeEO`,指向子 VM 的卷。 - -**关键 FK 约束**(基于 `V4.7.0__schema.sql` DDL): - -| 字段 | FK 目标 | ON DELETE | 含义 | -|------|---------|-----------|------| -| `ReferenceVO.referenceVolumeUuid` | `VolumeEO` | CASCADE | 子 VM 卷删除时级联删除引用记录 | -| `ReferenceVO.parentId` | 自身 `id` | SET NULL | 父引用删除后置 NULL | -| `ReferenceVO.treeUuid` | `ReferenceTreeVO` | SET NULL(DDL 实际值) | 树删除后置 NULL | -| `ReferenceVO.volumeUuid` | — | **无 FK** | 允许指向已删除的缓存 VM 卷 | -| `ReferenceVO.volumeSnapshotUuid` | — | **无 FK** | 允许指向已删除的缓存 VM 快照 | -| `ReferenceTreeVO.*` | — | **零 FK** | 完全独立表,所有字段均无外键约束 | - ---- - -## 3. 编码策略 — per-Resource 字段级 Base64 - -**设计决策**:DTO 整体为明文 JSON 写入存储介质,**不对整体 JSON 做 Base64 编码**。仅对每个 `ResourceMetadata` 中的 `systemTags` 和 `resourceConfigs` 字段采用 **per-Resource 整体 Base64 编码**。 - -- **sblk**:Slot Payload = DTO JSON 明文 -- **local/NFS**:文件内容 = DTO JSON 明文 - -**理由**: - -1. 避免整体 Base64 带来的 4/3 空间膨胀 -2. SystemTag/ResourceConfig 可能含特殊字符,Base64 避免 JSON 转义问题 -3. 主体数据保持明文,`APIReadVmInstanceMetadataFromPrimaryStorageMsg` 可直接读取完整 JSON -4. 一致性检查 API 可直接对主体数据做结构化比较 -5. per-Resource 整体编码减少 Base64 header 开销和解码次数 - -**容量说明**:Base64 对原始数据体积膨胀约 **33%**。当前方案仅对 `systemTags/resourceConfigs` 做字段级编码,整体可控。 - -**编解码流程**: - -``` -写入: - 对每个 ResourceMetadata: - filteredTags = 按白名单过滤 List - filteredConfigs = 按白名单过滤 List - systemTags = Base64( JSON.toJsonString( sorted(filteredTags, by uuid) ) ) - resourceConfigs = Base64( JSON.toJsonString( sorted(filteredConfigs, by uuid) ) ) - DTO → JSON → 写入存储 - -读取: - 存储 → JSON → DTO - 对每个 ResourceMetadata: - List = JSON.parseArray( Base64Decode(systemTags) ) - List = JSON.parseArray( Base64Decode(resourceConfigs) ) -``` - ---- - -## 4. 序列化关注点 - -| 关注点 | 方案 | -|--------|------| -| 嵌套 JSON 转义 | 保持 String 类型,Gson 自动处理双重转义/反转义 | -| JSON 字段顺序一致性 | 所有 DTO 字段使用 `@SerializedName` 注解显式命名并按声明顺序输出。**设计决策理由(讨论 Δ-序列化)**:纯依赖 Java 字段声明顺序在重构时有序变风险,`@SerializedName` 固化字段名使 JSON key 不受 Java 重命名影响,同时 Gson 按声明顺序输出已满足确定性要求,无需额外 `@Order` 注解 | -| null 字段处理 | Gson 默认跳过 null 字段(`new Gson()` 不输出 null),反序列化时 Java 默认值与 null 语义一致。**设计决策理由(讨论 Δ-null)**:DTO 中值为 null 的字段在序列化后的 JSON 中不存在对应 key,反序列化时 Java 字段保持声明默认值(引用类型为 null,基本类型为 0/false),语义等价,无需特殊处理 | -| SystemTag/ResourceConfig 写入策略 | 构建时按白名单过滤(见 §4.1),仅写入注册时需要恢复的 tag/config | -| 元数据大小 | 极端场景(24盘×256快照)约 5-10MB,仅 SystemTag/ResourceConfig 字段 Base64 编码,整体膨胀可忽略,在 sblk 单 Slot 32MB 内 | -| 不压缩的理由 | 正常场景 <100KB,极端场景罕见;压缩会增加 Agent 依赖和调试复杂度;未来可通过 bump HeaderVersion 引入 CompressionType 字段支持 | -| VO JSON 字段范围 | 包含所有非 `@Transient` 持久化字段;注册时 `id`(自增主键)由 DB 重新生成,`createDate` 保留原值,`lastOpDate` 替换为注册时间 | - -### 4.1 SystemTag/ResourceConfig 构建时过滤规则 - -**构建时:白名单过滤** - -序列化时按白名单过滤,仅将影响 VM 注册恢复的 SystemTag 和 ResourceConfig 写入元数据。 - -1. 白名单复用已有的 `CoreMemorySnapshotConfigs`(内存快照恢复功能维护的白名单,见下方),影响 VM XML 的 tag 和 config -2. 初始 SystemTag 白名单:`USERDATA`、`SSHKEY`、`BOOT_MODE`、`HOSTNAME`、`CPU_CORES`、`MACHINE_TYPE`、`VIRTIO` 等 -3. 初始 ResourceConfig 白名单:`NESTED_VIRTUALIZATION`、`VM_CPU_QUOTA`、`VM_CLOCK_TRACK`、`LIBVIRT_CACHE_MODE` 等 -4. 可通过 `@NeedRestoreOnVmApplySnapshot` 注解自动扩展白名单 -4. 未命中白名单的 SystemTag/ResourceConfig 不写入元数据 - -**注册时:直接恢复,无需二次过滤** - -> **设计决策**:元数据中的 SystemTag 和 ResourceConfig 已在构建时经过白名单过滤,注册恢复时直接持久化到 DB,**不再执行二次过滤**。 - -**理由**: -1. 构建时已过滤 → 元数据中只包含影响 VM XML 的 tag/config -2. 二次过滤无意义——白名单是同一套规则 -3. 升级后白名单扩展时,已有 full-refresh 覆盖全量元数据 - -**白名单定义**(复用已有的 `CoreMemorySnapshotConfigs`,无需重复维护): - -> 内存快照恢复功能已有完整的 SystemTag / ResourceConfig 白名单定义(见 `CoreMemorySnapshotConfigs.java`),元数据功能直接复用,保证两个场景的白名单始终一致。 - -```java -// 元数据构建时直接使用 CoreMemorySnapshotConfigs 的白名单过滤: -// SystemTag 过滤: -CoreMemorySnapshotConfigs.restoreCandidatePatternedSystemTags // PatternedSystemTag 列表 -CoreMemorySnapshotConfigs.restoreCandidateSystemTags // SystemTag 列表 - -// ResourceConfig 过滤——按资源类型分组: -CoreMemorySnapshotConfigs.vmRestoreCandidateConfigs // VM 级别的 GlobalConfig -CoreMemorySnapshotConfigs.volumeRestoreCandidateConfigs // Volume 级别的 GlobalConfig -CoreMemorySnapshotConfigs.vmNicRestoreCandidateConfigs // VmNic 级别的 GlobalConfig - -// 新增白名单条目两种方式: -// 1. 在 CoreMemorySnapshotConfigs 静态列表中直接添加 -// 2. 在 GlobalConfig 字段上标注 @NeedRestoreOnVmApplySnapshot 注解(自动收集) -``` - -> 新增白名单条目 → 修改 `CoreMemorySnapshotConfigs` 或添加 `@NeedRestoreOnVmApplySnapshot` 注解。内存快照恢复和元数据注册两个场景同步受益。 -> CI 可维护性保证:`MetadataWhitelistChecker`(统一 CI 检查)见 [Part 1b §3](vm-metadata-01b-API拦截与VM解析.md#3-统一-ci-检查--metadatawhitelistchecker)。 - -**演进说明(讨论澄清)**:`CoreMemorySnapshotConfigs` 当前命名绑定内存快照恢复场景。随着元数据功能上线,建议后续将其重构为更通用的命名(如 `VmConfigRestoreCandidates` 或 「影响虚拟机 XML 的配置」),统一表达「影响 VM 运行时配置、需要在恢复场景中还原的 Tag/Config 列表」语义。重构仅涉及类名和引用点,不改变白名单内容和收集逻辑。 - -**演进备注**:`buildVmInstanceMetadata()` 当前查询源列表为显式实现,v2+ 可提取为 SPI(如 `VmMetadataBuildSource`)以支持插件化扩展额外 VO/资源采集源。 - ---- - -## 5. 反序列化关注点 - -| 关注点 | 方案 | -|--------|------| -| 二步反序列化 | 先反序列化 DTO,再反序列化各 ResourceMetadata.vo 为具体 VO 类 | -| resourceUuid 一致性校验 | 反序列化后校验 `resourceMetadata.resourceUuid == parsedVO.getUuid()` | -| VO 字段版本兼容(多字段) | Gson 忽略缺失字段,填充 Java 默认值 | -| VO 字段版本兼容(少字段) | Gson 忽略未知字段 | -| Base64 解码失败 | systemTags/resourceConfigs 元素 Base64 解码失败时直接报错拒绝注册,不做部分恢复 | - ---- - -## 6. schemaVersion 版本规则 - -### 6.1 版本号定义 - -`schemaVersion` 使用 ZStack 数据库版本号,即 **`dbf.getDbVersion()`** 的返回值,与数据库 schema 版本完全一致。 - -**版本号格式说明(讨论澄清)**:`dbf.getDbVersion()` 返回纯数字版本号(如 `"4.7.0"`),不含 `V` 前缀或 `__schema` 等后缀。该值直接来源于 `DatabaseFacade` 的版本查询,调用方无需额外清洗或裁剪。 - -### 6.2 比较规则 - -> **统一规则**:注册 API 和预检查 API 均使用精确匹配:`metadata.schemaVersion == dbf.getDbVersion()`。 - -| 场景 | 行为 | -|------|------| -| `metadata.schemaVersion == dbf.getDbVersion()` | 匹配,正常注册 | -| 不匹配 + `forceVersionMismatch=false` | 拒绝(`METADATA_SCHEMA_VERSION_MISMATCH`) | -| 不匹配 + `forceVersionMismatch=true` | 允许注册,缺失字段置 null,warnings 记录 | - -**`forceVersionMismatch=true` 行为精确定义(讨论澄清)**:该标志**仅跳过 schemaVersion 精确匹配检查**,所有其他校验(UUID 冲突、installPath 存在性、readStatus 可用性、跨存储、vmCategory 类型等)均正常执行,不受此标志影响。缺失字段由 Gson 反序列化自动填充 Java 默认值(null/0/false),多余字段由 Gson 自动忽略。 - -### 6.3 版本生命周期 - -- 序列化时由 `VmMetadataBuilder` 自动填充 `dbf.getDbVersion()` -- 升级后通过全量刷新(批量 `markDirty`,Poller 自动处理)将所有 VM 元数据更新到新版本 -- 刷写端不依赖旧版本元数据,从 DB 直接构建新版本元数据覆盖写入 - -### 6.4 升级时间窗口 - -- 升级后批量 `markDirty` 所有已启用元数据的 VM,Poller 自动分批处理(详见 [Part 2b §9](vm-metadata-02b-高可用与运维.md#9-升级后全量刷新)) -- 10 万 VM × 平均 50ms/VM ≈ 5000 秒 ≈ 83 分钟 -- 窗口期内若需注册 VM,可使用 `APIUpdateVmMetadataMsg` 单独更新指定 VM 的元数据 - -### 6.5 vmCategory 兼容性 - -`vmCategory` 是新增字段,需要 bump `schemaVersion`: -- **新版本写入的元数据**:包含 `vmCategory` 字段 -- **旧版本写入的元数据**:不含该字段,Gson 反序列化时 `vmCategory` 为 `null` -- **注册时处理**:`vmCategory == null` 视为 `REGULAR`(向后兼容) - ---- - -## 7. VmCdRomVO 等附属资源 - -当前版本不纳入元数据。理由: - -- CD-ROM 挂载状态通常不影响 VM 恢复启动 -- USB/PCI 透传设备与宿主机绑定,跨环境无意义 -- 后续版本如需支持,通过 `VmInstanceMetadataDTO` 新增字段 + bump schemaVersion diff --git "a/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" "b/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" deleted file mode 100644 index 98a11eef395..00000000000 --- "a/docs/design/vm-metadata-01b-API\346\213\246\346\210\252\344\270\216VM\350\247\243\346\236\220.md" +++ /dev/null @@ -1,433 +0,0 @@ -# VM 元数据 — API 拦截与 VM 解析 - -## 目录 - -1. [@MetadataImpact 注解与 API 拦截](#1-metadataimpact-注解与-api-拦截) - - [1.5 条件性影响(v2+)](#15-条件性影响v2) - - [1.6 内部消息覆盖策略](#16-内部消息覆盖策略) - - [1.7 pendingApis 生命周期治理](#17-pendingapis-生命周期治理) -2. [VmUuid 解析器(Resolver)](#2-vmuuid-解析器resolver) - - [2.4 Resolver → API 映射表](#24-resolver--api-映射表) -3. [统一 CI 检查 — MetadataWhitelistChecker](#3-统一-ci-检查--metadatawhitelistchecker) - - [3.1 CI 扩展:内部消息 markDirty 审计](#31-ci-扩展内部消息-markdirty-审计) - - [3.2 设计决策:为什么不用 ExtensionPoint](#32-设计决策为什么不用-extensionpoint-监听-tagconfig-变更) -4. [影响虚拟机元数据的 API 清单](#4-影响虚拟机元数据的-api-清单) -5. [约束与不変量](#5-约束与不変量) - ---- - -## 1. @MetadataImpact 注解与 API 拦截 - -### 1.1 注解定义 - -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface MetadataImpact { - MetadataImpactLevel value() default MetadataImpactLevel.CONFIG; - Class resolver() default DirectVmUuidResolver.class; - boolean updateOnFailure() default false; -} - -public enum MetadataImpactLevel { - NONE, // 无关虚拟机配置 - CONFIG, // 虚拟机普通配置更新 - STORAGE // 虚拟机存储结构更新(快照/存储迁移等) -} -``` - -### 1.2 拦截范围(重要) - -**所有 `APIMessage` 子类必须标注 `@MetadataImpact`,CI 强制检查。** - -1. `MetadataWhitelistChecker` 扫描所有 `APIMessage` 子类,**未标注 `@MetadataImpact` 的 API 直接 CI 报错** -2. 开发者必须为每个 API 显式赋值 `NONE`、`CONFIG` 或 `STORAGE` -3. 运行时拦截器只对 level ≠ `NONE` 的 API 触发元数据更新 - -**opt-out 策略说明**(存量 API 标注): -- `@MetadataImpact(NONE)` — 与 VM 配置无关的 API(网络管理、用户管理等) -- `@MetadataImpact(STORAGE)` — 涉及存储拓扑变更的 API(快照、存储迁移、卷加卸等) -- `@MetadataImpact` 或 `@MetadataImpact(CONFIG)` — 其余 VM 相关配置更新 API - -### 1.3 STORAGE 标记的精确定义 - -`@MetadataImpact(STORAGE)` 表示该 API 导致 VM 的**存储结构**发生变化。以下**任一条件**成立即判定: - -| # | 条件 | 典型 API | -|---|------|----------| -| 1 | VM 的卷列表发生变化(数量增减) | Attach/Detach/DeleteDataVolume | -| 2 | 任一卷的 installPath 发生变化 | StorageMigrate, Reimage, Flatten | -| 3 | 任一卷的快照数量发生变化 | Create/DeleteSnapshot, SnapshotGroup | - -**不属于存储结构变化的场景**:卷大小变更(resize)—— installPath 不变、快照不变、卷数不变,归类为 `CONFIG`。 - -**对 sblk 的影响**:`@MetadataImpact(CONFIG)` → OP type=1(`CONFIG_UPDATE`),`@MetadataImpact(STORAGE)` → OP type=2(`STORAGE_CHANGE`)。OP type 通过 `storageStructureChange` 字段贯穿整条消息链。详见 [Part 4c §2](vm-metadata-04c-sblk写入流程.md#2-核心流程三阶段原子写入)。 - -### 1.4 updateOnFailure 触发条件 - -| 条件 | 是否触发 | -|------|----------| -| `updateOnFailure=true` + API 成功 | 触发 | -| `updateOnFailure=true` + API 失败(实际执行了部分逻辑) | 触发 | -| `updateOnFailure=true` + API 参数校验失败(未进入业务逻辑) | **不触发** | -| `updateOnFailure=false` + API 失败 | 不触发 | - -**关键实现 — `__metadata_vmUuids__` 的设置时机**: - -`__metadata_vmUuids__` 是 ThreadContext 中的标记键,由 `BeforeDeliveryMessageInterceptor` 在 API 参数校验通过后设置。其存在性即"是否进入过业务逻辑"的可靠标志。纯参数校验失败不经过拦截器,ThreadContext 中无此键,自然不触发 `updateOnFailure`。 - -``` -时序流程: - CloudBus 收到 APIMessage - ├─ 参数校验失败 → 直接返回错误 → ThreadContext 无 __metadata_vmUuids__ - └─ 参数校验通过 → 进入 BeforeDeliveryMessageInterceptor - ├─ @MetadataImpact level ≠ NONE → Resolver.resolve(msg) → 存入 ThreadContext + pendingApis - └─ API 业务逻辑执行 - ├─ 成功 → beforePublishEvent → markDirty - └─ 失败 → 检查 updateOnFailure + __metadata_vmUuids__ → 满足则 markDirty -``` - -### 1.5 条件性影响(v2+) - -Q1b-5 结论:现阶段保持 `@MetadataImpact` 简单枚举语义(`NONE/CONFIG/STORAGE`),不在 v1/v1.1 引入条件表达式。 - -| 项 | v1/v1.1 决策 | v2+ 预留 | -|----|--------------|----------| -| 注解模型 | 固定 `MetadataImpactLevel` + 固定 Resolver | 可扩展 `condition` 或策略接口 | -| 风险控制 | 采用保守标注:不确定场景优先 `CONFIG`/`STORAGE` 而非 `NONE` | 引入运行时条件判定避免过度刷新 | -| 文档约束 | 任何“条件性”需求先落入 API 评审清单并记录原因 | 条件模型落地需单独 RFC | - -### 1.6 内部消息覆盖策略 - -`@MetadataImpact` 仅覆盖 `APIMessage`。对内部消息(`Message` 子类)采用**显式注册 + 代码审计**: - -```java -private static final Set> INTERNAL_METADATA_MESSAGES = Set.of( - AllocateHostMsg.class, - MigrateVmMsg.class, - ChangeVmIpMsg.class, - DetachDataVolumeFromVmMsg.class, - DeleteVolumeSnapshotMsg.class - // 仅示例:实际清单以 vm-instance / storage / network 相关处理器审查结果为准 -); -``` - -- 注册表用于声明“已知会影响元数据且不经过 API 拦截器”的消息类型。 -- 对应 handler 在事务提交后调用 `markDirty()`,避免 dirty mark 指向未提交快照。 -- Poller 的 stale 窗口为设计内窗口,不消除;通过“提交后 markDirty + 下一轮全量构建”收敛(见 [Part 2 §1.4](vm-metadata-02-脏标记与Poller.md#14-最终一致性模型))。 - -### 1.7 pendingApis 生命周期治理 - -`pendingApis` 为 `ConcurrentHashMap`,新增超时治理(超时时间可通过 GlobalConfig 配置),防止 API 超时导致 entry 泄漏: - -1. 每 5 分钟执行一次清理任务。 -2. 移除“创建时间 > `VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES`(默认 45 分钟)”的 entry。 -3. 对被清理 entry 的 vmUuid 执行 `markDirty()`(最终一致)。 -4. `afterCompletion` 增加 null check:`remove(apiId)` 返回 null 时按“已被清理”分支继续,不报错。 - -**Per-API 超时策略(讨论 Δ-3)**:原方案使用固定 45 分钟超时,无法匹配 API 类型差异。改为从 `PendingApiContext` 中记录 API 类名,清理时根据 API 类型动态计算超时: -- 普通 API:使用 API 自身的 `timeout` 字段(若该 API 为 LongJob 触发,则取 LongJob 超时)。 -- 回退默认值:若 API 无显式 timeout 配置,使用 `VM_METADATA_PENDING_API_TIMEOUT_MINUTES`(默认 45min)。 -- 此设计确保 LongJob 场景(如存储迁移可达数小时)不会被过早清理,同时普通短 API 不会等待过久才触发 markDirty。 - -**MN 重启时 pendingApis 丢失的处理策略**:`pendingApis` 是 JVM 内存结构,MN 重启后全部丢失。对正在执行中的 API,有以下几种情况: -- **API 执行已到达 `afterCompletion`**:`remove(apiId)` 返回 null → 按"已清理"分支处理 → 对 vmUuids 执行 `markDirty()`,保证最终一致。由于 MN 已重启,此路径不会执行。 -- **API 尚未完成即 MN 崩溃**:API 执行被中断,`markDirty()` 未被调用。恢复依赖两条路径:(1) 用户重新发起 API → 新的 API 触发 `markDirty()`;(2) 路径指纹巡检(Part 2b §8.2)发现漂移 → 自动 `markDirty()`。 -- **结论**:MN 重启丢失 pendingApis 不会导致数据永久不一致,最终一致性由 Poller + 路径巡检保证。无需持久化 pendingApis(持久化成本高于收益)。 - -**`updateOnFailure` 与 pendingApis 的交互**:`updateOnFailure=true` 的 API 在失败时通过 `afterCompletion(reply)` 回调处理。回调从 `pendingApis.remove(apiId)` 取出预缓存的 vmUuids,检查 `reply.isSuccess()` 为 false,若 `updateOnFailure=true` 则执行 `markDirty()`。与成功路径使用同一 pendingApis entry,无额外数据结构。若 entry 已被超时清理,`remove()` 返回 null,此时 vmUuids 已在清理时被 `markDirty()` 过,不会遗漏。 -```java -scheduledPool.scheduleAtFixedRate(() -> { - Instant deadline = Instant.now().minus(Duration.ofMinutes(VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES.value(Long.class))); - pendingApis.entrySet().removeIf(e -> { - if (e.getValue().getCreateTime().isBefore(deadline)) { - e.getValue().getVmUuids().forEach(vm -> markDirty(vm, e.getValue().isStorageStructureChange())); - return true; - } - return false; - }); -}, 5, 5, TimeUnit.MINUTES); -``` - ---- - -## 2. VmUuid 解析器(Resolver) - -### 2.1 解析时机 - -Resolver 在**两个时机**捕获 vmUuid: - -1. **API 执行前**(`BeforeDeliveryMessageInterceptor`):预解析 vmUuid 并缓存到 `pendingApis` ConcurrentHashMap 中(key = apiId) -2. **API 成功后**(`beforePublishEvent`):从 `pendingApis` 读取缓存的 vmUuid,调用 `markDirty(vmUuid)` 标脏 - -**执行线程说明**:`beforeDeliveryMessage()` 在消息投递线程中同步执行。Resolver 的 DB 查询在此线程中执行,对单次 API 延迟影响极小(<1ms)。 - -### 2.2 Resolver 接口 - -```java -public interface VmUuidFromApiResolver { - List resolve(APIMessage msg); -} -``` - -### 2.3 内置 Resolver 实现 - -| Resolver | 逻辑 | -|----------|------| -| `DirectVmUuidResolver` | 从 `msg.getVmInstanceUuid()` 直接获取 | -| `VolumeToVmResolver` | 通过 volumeUuid 查 `VolumeVO.vmInstanceUuid` | -| `PreCaptureVolumeToVmResolver` | 同 VolumeToVmResolver,标记为需要预捕获(API 执行前获取) | -| `SnapshotToVmResolver` | snapshotUuid → VolumeSnapshotVO.volumeUuid → VolumeVO.vmInstanceUuid | -| `SnapshotGroupToVmResolver` | groupUuid → refs → 多个 volumeUuid → 多个 vmUuid | -| `ResourceUuidToVmResolver` | resourceUuid 可能是 VM/Volume/NIC,逐一判断 | - -**`ResourceUuidToVmResolver` 过滤非 VM 相关资源**:`APICreateSystemTagMsg` 等 Tag API 的 `resourceUuid` 可能指向任意资源类型(Host、Zone、L3Network 等)。`ResourceUuidToVmResolver` 的实现按以下优先级解析: -1. `dbf.findByUuid(resourceUuid, VmInstanceVO.class)` → 非 null 则直接返回 vmUuid -2. `dbf.findByUuid(resourceUuid, VolumeVO.class)` → 取 `vmInstanceUuid` -3. `dbf.findByUuid(resourceUuid, VmNicVO.class)` → 取 `vmInstanceUuid` -4. 以上均为 null → 返回空列表(该 Tag 不关联 VM,跳过 markDirty) - -此为已有实现的显式文档化。每步查询命中索引,开销 < 1ms。非 VM 相关 Tag(如 Host Tag)在第 4 步返回空,不触发任何元数据操作。 - -| `NicToVmResolver` | nicUuid → VmNicVO.vmInstanceUuid | -| `PreCaptureNicToVmResolver` | 同 NicToVmResolver,标记为需要预捕获 | - -### 2.4 Resolver → API 映射表 - -该表用于代码评审和 CI 问题定位;权威 API 列表以 §4 为准。 - -| Resolver | 典型 API | 说明 | -|----------|----------|------| -| `DirectVmUuidResolver` | `APIUpdateVmInstanceMsg`、`APISetVmBootOrderMsg`、`APIReimageVmInstanceMsg`、`APICloneVmInstanceMsg` | API 入参直接包含 vmUuid | -| `VolumeToVmResolver` | `APIAttachDataVolumeToVmMsg`、`APIRecoverDataVolumeMsg`、`APIPrimaryStorageMigrateVolumeMsg`、`APIResizeDataVolumeMsg` | 通过 volumeUuid 反查 VM | -| `PreCaptureVolumeToVmResolver` | `APIDetachDataVolumeFromVmMsg`、`APIDeleteDataVolumeMsg` | 删除/卸载场景需 API 前预捕获 | -| `SnapshotToVmResolver` | `APIDeleteVolumeSnapshotMsg`、`APIRevertVolumeFromSnapshotMsg` | snapshotUuid → volumeUuid → vmUuid | -| `SnapshotGroupToVmResolver` | `APIDeleteVolumeSnapshotGroupMsg` | groupUuid 可映射多个 VM(跨卷) | -| `ResourceUuidToVmResolver` | `APICreateSystemTagMsg`、`APIDeleteTagMsg`、`APIUpdateResourceConfigMsg` | 资源类型可能是 VM/Volume/NIC,需多分支解析 | -| `NicToVmResolver` | `APIChangeVmNicNetworkMsg`、`APIChangeVmNicStateMsg`、`APIDetachNicFromBondingMsg` | nicUuid → vmUuid | -| `PreCaptureNicToVmResolver` | `APIDeleteVmNicMsg` | 删除场景需 API 前预捕获 | - ---- - -## 3. 统一 CI 检查 — MetadataWhitelistChecker - -**注意**:此为唯一的 CI 检查类,合并了 API 注解检查、SystemTag 白名单检查、ResourceConfig 白名单检查。 -白名单数据复用 `CoreMemorySnapshotConfigs`(已有的内存快照恢复候选列表),不再另建 Provider 接口。 - -```java -public class MetadataWhitelistChecker extends PostBuildCheckerCase { - @Override - public void check() { - // Part 1: API @MetadataImpact 注解 + Resolver 检查 - Set> allApiMsgs = BeanUtils.reflections.getSubTypesOf(APIMessage.class); - for (Class msgClass : allApiMsgs) { - if (isQueryOrGetApi(msgClass)) continue; - assertMetadataImpactPresent(msgClass); // 注解必须存在 - assertResolverValid(msgClass); // level ≠ NONE 时检查 Resolver - } - - // Part 2: SystemTag 白名单检查(数据来源:CoreMemorySnapshotConfigs) - Set allDefinedTags = scanAllSystemTagDefinitions(); - Set registeredTags = new HashSet<>(); - registeredTags.addAll(toTagNames(CoreMemorySnapshotConfigs.restoreCandidatePatternedSystemTags)); - registeredTags.addAll(toTagNames(CoreMemorySnapshotConfigs.restoreCandidateSystemTags)); - // @NeedRestoreOnVmApplySnapshot 注解标注的 Tag 自动纳入 - registeredTags.addAll(collectAnnotatedTags(NeedRestoreOnVmApplySnapshot.class)); - for (String tag : allDefinedTags) { - if (!registeredTags.contains(tag)) { - fail("SystemTag '" + tag + "' not in CoreMemorySnapshotConfigs whitelist"); - } - } - - // Part 3: ResourceConfig 白名单检查(数据来源:CoreMemorySnapshotConfigs) - Set allConfigCategories = scanAllResourceConfigCategories(); - Set registeredCategories = new HashSet<>(); - registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.vmRestoreCandidateConfigs)); - registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.volumeRestoreCandidateConfigs)); - registeredCategories.addAll(toConfigNames(CoreMemorySnapshotConfigs.vmNicRestoreCandidateConfigs)); - for (String cat : allConfigCategories) { - if (!registeredCategories.contains(cat)) { - fail("ResourceConfig '" + cat + "' not in CoreMemorySnapshotConfigs whitelist"); - } - } - } -} -``` - -### 3.1 CI 扩展:内部消息 markDirty 审计 - -在现有三段检查基础上,补充 Part 4 审计(对 STORAGE 级内部消息为 ERROR 阻断级,其余为 WARNING): - -1. 扫描 `AbstractHandler` / `MessageHandler` 实现。 -2. 若检测到 VM 相关 VO(`VmInstanceVO`/`VolumeVO`/`VolumeSnapshotVO`/`VmNicVO`)写操作且未调用 `markDirty()`,输出 WARNING。 -3. 若 handler 处理的消息类型命中 `INTERNAL_METADATA_MESSAGES`,但未在注册表注释中标注触发来源,输出 WARNING。 -4. **STORAGE 级阻断**:若 handler 处理的消息类型命中 `INTERNAL_METADATA_MESSAGES` 且该消息在注册表中标注为 `STORAGE` 级别,但 handler 未调用 `markDirty()`,则**输出 ERROR 并阻断 CI 构建**(`fail()`),而非仅 WARNING。原因:STORAGE 级遗漏会导致 sblk OP type 错误,影响存储拓扑一致性,风险远高于 CONFIG 级遗漏。 - -**说明**:CONFIG 级内部消息的检查仍为"辅助发现"WARNING,不阻断 CI;STORAGE 级为强制阻断 ERROR。此区分确保高风险路径不被遗漏,同时避免对低风险基础设施路径(升级脚本、巡检修复)误杀。 - -**CI 报错引导示例**: -``` -"API APIAttachGpuDeviceToVmMsg 的 resolver GpuToVmResolver 未找到。 - 请实现 VmUuidFromApiResolver 接口,从该 API 消息中解析出关联的 vmUuid。 - 参考内置实现:NicToVmResolver、VolumeToVmResolver 等。" -``` - ---- - -### 3.2 设计决策:为什么不用 ExtensionPoint 监听 Tag/Config 变更? - -**背景**:ZStack 内部 SystemTag 的修改路径主要有三类: - -| 路径 | 频次 | 是否触发 lifecycle callback | -|------|------|---------------------------| -| `TagManager.newSystemTagCreator().create()` | ~263 处 | (Y) 触发 | -| `SystemTag.delete()` | ~143 处 | (Y) 触发 | -| `SQL.New(SystemTagVO.class)` / `dbf.persist(SystemTagVO)` | ~15 处 | (N) 完全绕过 | - -**结论:不额外引入 `SystemTagLifeCycleExtensionPoint`**,理由如下: - -1. **用户发起的 Tag/Config 修改全部通过 API**(§4.1/§4.2 已列出),API 拦截器已覆盖 -2. **内部 Tag 操作发生在已标注 `@MetadataImpact` 的 API 执行上下文中**(如 `APICreateVmInstanceMsg` 流程内的 `setBootMode` 系统标签),上层 API 已触发 `markDirty` -3. **~15 处直接 SQL 操作**均为基础设施级别(Host 重连写硬件信息、升级迁移脚本、IAM 操作),不涉及 `CoreMemorySnapshotConfigs` 中的元数据相关 Tag(USERDATA、SSHKEY、BOOT_MODE 等) -4. **即使极端情况遗漏**,Poller 安全网(Part 2 §4)从 DB 全量构建 DTO 写入,最终一致 -5. **ExtensionPoint 成本高**:需在 ~500+ 内部操作中逐一过滤白名单,不值得 - -ResourceConfig 同理:`ResourceConfig.updateValue()` 内部调用 ~92 处触发 extension,`SQL.New(ResourceConfigVO.class)` 直接操作仅 2 处(基础设施级别)。API 拦截 + Poller 安全网已足够。 - ---- - -## 4. 影响虚拟机元数据的 API 清单 - -以下列出所有需要标注 `@MetadataImpact` 且 level ≠ `NONE` 的 API。 - -### 4.1 SystemTag 相关 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APICreateSystemTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | -| `APIDeleteTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | -| `APIUpdateSystemTagMsg` | CONFIG | `ResourceUuidToVmResolver` | false | -| `APISetVmBootOrderMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIDeleteVmBootModeMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIDeleteVmSshKeyMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIDeleteVmHostnameMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APISetVmQgaMsg` | CONFIG | `DirectVmUuidResolver` | false | - -### 4.2 ResourceConfig 相关 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APIUpdateResourceConfigMsg` | CONFIG | `ResourceUuidToVmResolver` | false | -| `APIDeleteResourceConfigMsg` | CONFIG | `ResourceUuidToVmResolver` | false | - -### 4.3 磁盘加载卸载 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APIAttachDataVolumeToVmMsg` | STORAGE | `VolumeToVmResolver` | false | -| `APIDetachDataVolumeFromVmMsg` | STORAGE | `PreCaptureVolumeToVmResolver` | false | -| `APIDeleteDataVolumeMsg` | STORAGE | `PreCaptureVolumeToVmResolver` | false | -| `APIRecoverDataVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | -| `APIReimageVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | false | - -### 4.4 存储迁移 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APIPrimaryStorageMigrateVmMsg` | STORAGE | `DirectVmUuidResolver` | false | -| `APIPrimaryStorageMigrateVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | -| `APILocalStorageMigrateVolumeMsg` | STORAGE | `VolumeToVmResolver` | **true** | - -### 4.5 快照相关 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APICreateVolumesSnapshotMsg` | STORAGE | `VolumeToVmResolver` | false | -| `APICreateVolumeSnapshotGroupMsg` | STORAGE | `DirectVmUuidResolver` | **true** | -| `APIDeleteVolumeSnapshotMsg` | STORAGE | `SnapshotToVmResolver` | false | -| `APIDeleteVolumeSnapshotGroupMsg` | STORAGE | `SnapshotGroupToVmResolver` | **true** | -| `APIRevertVolumeFromSnapshotMsg` | STORAGE | `SnapshotToVmResolver` | false | -| `APIFlattenVolumeMsg` | STORAGE | `VolumeToVmResolver` | false | - -**审计结论**:`APICreateVolumeSnapshotGroupMsg`、`APILocalStorageMigrateVolumeMsg`、`APIDeleteVolumeSnapshotGroupMsg` 为批量/部分成功风险 API,统一要求 `updateOnFailure=true`。 - -| API | 风险类型 | updateOnFailure 要求 | -|-----|----------|----------------------| -| `APICreateVolumeSnapshotGroupMsg` | 多卷快照,可能部分卷成功 | **true** | -| `APILocalStorageMigrateVolumeMsg` | 迁移流程分段执行,可能部分生效 | **true** | -| `APIDeleteVolumeSnapshotGroupMsg` | 组内快照删除可能部分成功 | **true** | - -### 4.6 克隆/模板 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APICloneVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | **true** | -| `APICreateTemplatedVmInstanceFromVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | **true** | -| `APICreateVmInstanceFromTemplatedVmInstanceMsg` | STORAGE | `DirectVmUuidResolver` | false | -| `APIExportImageFromBackupStorageMsg` | NONE | — | false | - -**说明**:Clone/Template 的 Resolver 解析**源 VM** UUID。新建 VM 的元数据由新建流程末尾自动生成。 - -**设计决策**:`APIExportImageFromBackupStorageMsg` 已确认为 `NONE`。导出镜像不修改 VM 配置/存储拓扑,不参与 resolver 解析与标脏链路。 - -### 4.7 模板虚拟机身份转换 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|------------------| -| `APIConvertVmInstanceToTemplatedVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIConvertTemplatedVmInstanceToVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | - -模板身份转换不改变存储拓扑,使用 CONFIG。元数据刷新时会重新计算 `vmCategory`。 - -### 4.8 卷大小变更 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|------------------| -| `APIResizeRootVolumeMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIResizeDataVolumeMsg` | CONFIG | `VolumeToVmResolver` | false | - -### 4.9 VM 配置变更 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|------------------| -| `APIUpdateVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false || `APIRecoverVmInstanceMsg` | CONFIG | `DirectVmUuidResolver` | false | - -**`APIRecoverVmInstanceMsg` 纳入说明(讨论补充)**:Recover VM 将 Destroyed 状态的 VM 恢复为 Stopped,该操作改变了 VM 状态但不涉及存储拓扑变更,因此标注为 `CONFIG`。Recover 后 VM 需要重新刷写元数据(Destroyed 状态期间元数据可能已被 Poller 删除)。 -### 4.10 网卡相关 - -| API | Level | Resolver | updateOnFailure | -|-----|-------|----------|-----------------| -| `APIChangeVmNicNetworkMsg` | CONFIG | `NicToVmResolver` | false | -| `APIAttachVmNicToVmMsg` | CONFIG | `DirectVmUuidResolver` | false | -| `APIChangeVmNicStateMsg` | CONFIG | `NicToVmResolver` | false | -| `APIDeleteVmNicMsg` | CONFIG | `PreCaptureNicToVmResolver` | false | -| `APIDetachNicFromBondingMsg` | CONFIG | `NicToVmResolver` | false | -| `APIAttachNicToBondingMsg` | CONFIG | `NicToVmResolver` | false | - -### 4.11 VM 创建与未纳入元数据的 API - -**VM 创建**:`APICreateVmInstanceMsg` 不通过 `@MetadataImpact` 拦截器触发。VM 创建 FlowChain 末尾直接调用 `initializeMetadata()` + `markDirty()`,确保元数据文件在 VM 创建成功后即被初始化和首次刷写。 - -**VM 创建元数据初始化时机(讨论澄清)**:元数据初始化采用异步 post-success hook 模式,即在 `CreateVmInstanceFlow` 主 Flow 全部成功后、返回 API 结果前,通过异步回调执行 `markDirty(vmUuid, true)`。失败不影响 VM 创建结果,Poller 安全网会在后续轮次重试。此设计避免元数据写入失败导致整个 VM 创建回滚。 - -**CD-ROM**:`APIDeleteVmCdRomMsg` 标注为 `@MetadataImpact(NONE)`。CD-ROM 当前版本不纳入元数据(见 [Part 1a §7](vm-metadata-01a-数据模型与序列化.md#7-vmcdromvo-等附属资源)),删除 CD-ROM 不触发元数据更新。 - -## 5. 约束与不変量 - -**`INTERNAL_METADATA_MESSAGES` 完备性保证**:静态注册表无法自动发现新增内部消息。保证完备性的手段为三层防线: -1. **开发规范**:修改 VM 存储拓扑字段的内部消息 handler,成功后必须调用 `markDirty()`(Part 2b §12.4 D1) -2. **CI Part 4**:`MetadataWhitelistChecker` 扫描 handler 实现中的 VO 写操作 + markDirty 调用(§3.1),STORAGE 级遗漏为 ERROR 阻断 -3. **路径指纹巡检**:运行时兜底,检测实际路径漂移并自动 markDirty(Part 2b §8.2) - -无需为 `INTERNAL_METADATA_MESSAGES` 引入自动发现机制。CI + 运行时双重防线已提供足够保障。 - -**非 KVM Hypervisor 排除**:`@MetadataImpact` 标注在 `APIMessage` 类层面,不区分 Hypervisor 类型。运行时拦截器在 Resolver 解析出 vmUuid 后,**通过查询 `VmInstanceVO.hypervisorType` 过滤**:仅 `KVM` 类型的 VM 继续标脏,其他类型(VMware、Simulator 等)静默跳过。此过滤在 `markDirty()` 入口处实现(与 `type != "UserVm"` 检查同层),不增加 Resolver 复杂度。非 KVM VM 的存储驱动不支持元数据格式(无 sblk LV / 无 `.zstack-vm-metadata` 目录),跳过是正确行为。 - -| 约束 ID | 约束描述 | 违反后果 | -|---------|----------|----------| -| C-IC | `INTERNAL_METADATA_MESSAGES` 与内部 handler 的 `markDirty()` 调用点必须一一可追溯,新增内部消息需同步更新注册表与注释来源 | 内部路径修改被遗漏,Poller 长期读旧状态 | -| C-IM | 所有 `APIMessage` 子类必须显式标注 `@MetadataImpact`(可为 NONE);`MetadataWhitelistChecker` 扫描全量子类,不允许“默认未声明” | 新增 API 逃逸拦截链,行为不可预测 | -| C-PA | `pendingApis` 必须具备超时清理(5min 周期、可配超时(默认 45min))与 afterCompletion null-safe 逻辑;清理时需补 `markDirty()` | 内存泄漏或超时 API 的最终一致性断裂 | -| C-RS | Resolver 选择需与 API 资源语义匹配;删除/卸载类 API 必须使用 pre-capture resolver 或等价机制 | API 完成后资源已消失,无法解析 vmUuid 导致漏标脏 | -| C-H1 | `INTERNAL_METADATA_MESSAGES` 中标注为 STORAGE 级别的内部消息,其 handler 必须调用 `markDirty()`;CI Part 4 对此类遗漏执行 ERROR 阻断(`fail()`) | STORAGE 级内部路径遗漏 markDirty,sblk OP type 错误,存储拓扑一致性断裂 | -| C-M4 | `pendingApis` 超时时间必须通过 `VmGlobalConfig.VM_METADATA_PENDING_API_TIMEOUT_MINUTES` 配置(默认 45 分钟),不得硬编码 | 超时配置无法随 LongJob 场景调整,导致 entry 泄漏或过早清理 | diff --git "a/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" "b/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" deleted file mode 100644 index 84ffc2250fa..00000000000 --- "a/docs/design/vm-metadata-01c-\345\255\230\345\202\250\345\261\202\344\270\216\346\250\241\346\235\277\350\231\232\346\213\237\346\234\272.md" +++ /dev/null @@ -1,464 +0,0 @@ -# VM 元数据 — 存储层与模板虚拟机 - -## 目录 - -1. [存储层元数据](#1-存储层元数据) - 1.6. [存储迁移 Poller 暂停的崩溃恢复](#16-存储迁移-poller-暂停的崩溃恢复) -2. [模板虚拟机与链式克隆元数据](#2-模板虚拟机与链式克隆元数据) -3. [新增/修改代码文件清单](#3-新增修改代码文件清单) -4. [约束与不変量](#4-约束与不変量) - ---- - -## 1. 存储层元数据 - -### 1.1 元数据存储路径 - -| 存储类型 | 路径 | 约束 | -|----------|------|------| -| sblk | `/dev/{vg_uuid}/{vm_uuid}_vmmeta` | LV 名 `{vm_uuid}_vmmeta` 长度固定 39(32+7),远小于 LVM 名称上限 128 | -| local/NFS | `{mountPath}/.zstack-vm-metadata/{vm_uuid}.json` | 目录 `.zstack-vm-metadata` 必须以 `0700` 创建 | -| ceph/zbs/vhost | 当前版本不支持,后续按需扩展 | 不创建元数据容器 | - -### 1.2 各存储类型实现 - -**sblk(共享块存储)** - -详见 [Part 4a: sblk 概述](vm-metadata-04a-sblk存储协议概述.md) 及其子文档。核心要点: -- LV 命名:`{vm_uuid}_vmmeta` -- 长度安全性:`vm_uuid`(32 字符)+ `_vmmeta`(7 字符)= 39,低于 LVM 128 字符限制,无截断风险 -- 二进制格式:Header(4KB) + Slot A + Slot B -- 三阶段原子写入 -- 初始大小 4MB,阶梯式扩容至最大 64MB -- LV 初始化时写入 Header 并将完整 payload 写入 Slot A - -**local/NFS** - -- **`mountPath` 定义**:`mountPath = PrimaryStorageVO.url`。PS 所挂载集群的每台 Host 均有此挂载路径。 -- **NFS 前置条件**:NFS PS 的挂载选项已强制 `no_root_squash`(ZStack 创建 NFS PS 时校验并要求),因此 Agent 进程以 root 身份操作元数据文件无权限问题。 -- **目录创建**:`.zstack-vm-metadata` 目录的创建采用与 rootVolume 目录相同的逻辑(权限设置 `0700`,owner=root, group=root),防止非特权用户读取跨 VM 元数据文件。NFS 场景下目录自然跨 Host 共享;local 场景下各 Host 独立创建。 -- **文件路径**:`{mountPath}/.zstack-vm-metadata/{vm_uuid}.json`(集中式目录,便于扫描)。元数据文件跟随根盘所在 PS,即元数据锚定在根盘位置。 -- **初始文件**:`initializeMetadata` 创建元数据文件并写入当前 VM 的完整元数据 payload。若文件不存在则先创建再写入,若已存在则覆盖。创建流程同样使用 tmp + fsync + rename 原子写入路径。 -- **writeMetadata 容器自动创建(讨论 Δ-4)**:`writeMetadata` 执行前自动检查 `.zstack-vm-metadata/` 目录是否存在,不存在时自动创建(`mkdir -p` + `chmod 0700`)。此设计将 `initializeMetadata` 和 `writeMetadata` 的容器创建逻辑统一,无需调用方显式区分"首次写入"和"后续更新"。`initializeMetadata` 在语义上仍然保留(VM 创建场景的入口),但底层实现可直接复用 `writeMetadata` 路径。 -- **文件内容**:DTO JSON 明文(systemTags/resourceConfigs 为 per-Resource Base64 编码) -- **原子写入**:先写 tmp 文件 → `fsync(fd)` 刷盘 → `os.rename()` 替换 → `fsync(dirfd)` 刷新父目录元数据。`os.rename()` 等价于 Linux `mv`,原子替换目标文件,**rename 成功后 tmp 文件不会残留**(tmp 已变为目标文件)。仅在 write-tmp 完成后、rename 之前崩溃时,会残留一个 tmp 文件。`fsync(dirfd)` 保证 NFS 场景下目录项更新对其他客户端可见。并发安全性由 Poller CAS 认领机制保证(同一时刻只有一个 MN 持有某 VM 的 flush 权限),无需额外文件锁。 -- **tmp 文件命名**: - - 常规写入:`{vm_uuid}.json.tmp` - - 存储结构变更写入(`storageStructureChange=true`):`{vm_uuid}.json.sc.tmp` - - 使用固定命名(非随机),每次写入覆盖同名 tmp 文件,避免崩溃后积累多个残留文件。 -- **tmp 文件崩溃清理**:`os.rename()` 成功后 tmp 文件即消失(已成为目标文件),正常运行无残留。仅在崩溃窗口(write-tmp 完成 → rename 之前)会残留一个 `.tmp` 或 `.sc.tmp` 文件。Agent 启动时扫描 `.zstack-vm-metadata/` 目录中的 `*.tmp` 文件并删除即可。 -- **写入前 tmp 清理(讨论补充)**:`writeMetadata` 在写入新 tmp 文件前,先删除同名的旧 tmp 文件(若存在)。使用 `os.O_CREAT | os.O_TRUNC` 标志打开 tmp 文件天然实现覆盖,无需显式删除。此设计避免崩溃后 Agent 未重启时旧 tmp 残留影响后续写入。 -- **`storageStructureChange` 参数**:当 `storageStructureChange=true` 时,使用 `.sc.tmp` 后缀的 tmp 文件。此区分用于**注册时判断元数据是否可用**:若扫描到 `.sc.tmp` 残留,说明存储迁移写入未完成,该元数据文件的内容可能是迁移前的旧版本,注册时需标记为不可靠。写入逻辑本身(fsync + rename)无差异。 -- **完整性校验**:不设 checksum 字段。`rename` 是 POSIX 原子操作,不存在半写文件场景;JSON 解析成功即内容完整。读取时若 `json.loads()` 抛异常 → 视为损坏 → 日志告警 → `markDirty()` → 下轮 Poller 从 DB 全量重建 - -#### local/NFS 各操作异常分析 - -**writeMetadata — 原子写入各阶段异常** - -写入流程:`open(tmp)` → `write(payload)` → `fsync(fd)` → `close(fd)` → `os.rename(tmp, target)` → `open(dirfd)` → `fsync(dirfd)` → `close(dirfd)` - -| 阶段 | 异常类型 | 文件系统状态 | 处理方式 | -|------|---------|-------------|---------| -| **open(tmp) 失败** | `IOError`(磁盘满、权限、目录不存在) | 无 tmp 文件产生,`.json` 不受影响 | Agent 返回错误 → Poller 标记失败 → 指数退避重试 | -| **write(payload) 失败** | `IOError`(磁盘满、NFS 超时) | tmp 文件可能含部分数据,`.json` 不受影响 | `finally` 中 `close(fd)` + 尝试 `os.remove(tmp)`;Agent 返回错误 | -| **write 成功,fsync(fd) 失败** | `IOError`(NFS server 拒绝刷盘) | tmp 数据可能仅在 client 缓存中,`.json` 不受影响 | 同上:`close` + `remove(tmp)` + 返回错误 | -| **fsync 成功,rename 前 Agent 崩溃** | 进程崩溃/OOM/kill | tmp 文件完整残留在磁盘上,`.json` 为上次成功写入的版本 | Agent 重启时扫描 `*.tmp` 删除。Poller 下轮重试 | -| **os.rename(tmp, target) 失败** | `OSError`(极罕见:跨文件系统 rename、NFS stale handle) | tmp 完整存在,`.json` 为旧版本 | 尝试 `os.remove(tmp)` 清理;Agent 返回错误 → 重试 | -| **rename 成功,fsync(dirfd) 前 Agent 崩溃** | 进程崩溃 | local:数据已持久化(ext4/xfs rename 同步更新目录项)。NFS:目录项更新可能未刷到 server,但 NFS client 重连后会自动同步 | 无需特殊处理。NFS 最坏场景:其他 Host 短暂看到旧文件名 → 下轮 Poller 写入时 fsync(dirfd) 补齐 | -| **fsync(dirfd) 失败** | `IOError`(NFS server 异常) | `.json` 内容已正确(rename 已完成),仅目录元数据未保证刷到 server | Agent 日志告警但**视为成功返回**(数据完整性已由 rename 保证,dirfd fsync 仅影响跨客户端可见性延迟) | - -**关键不变量**:在写入流程的任何阶段崩溃或出错,`.json` 文件要么是上一次成功写入的完整版本,要么是本次新写入的完整版本。**不存在读到半写内容的可能**。 - -**writeMetadata — 异常处理伪代码** - -```python -def write_metadata(meta_dir, vm_uuid, payload, storage_structure_change): - target = os.path.join(meta_dir, f"{vm_uuid}.json") - suffix = ".sc.tmp" if storage_structure_change else ".tmp" - tmp = os.path.join(meta_dir, f"{vm_uuid}.json{suffix}") - - fd = None - try: - fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - os.write(fd, payload.encode('utf-8')) - os.fsync(fd) - os.close(fd) - fd = None - - os.rename(tmp, target) # 原子替换,此后 tmp 不再存在 - - # fsync(dirfd) — best-effort,失败不影响数据正确性 - try: - dirfd = os.open(meta_dir, os.O_RDONLY) - os.fsync(dirfd) - os.close(dirfd) - except OSError: - logger.warn(f"fsync(dirfd) failed for {meta_dir}, " - "NFS cross-client visibility may be delayed") - except Exception as e: - if fd is not None: - os.close(fd) - # 清理残留 tmp(rename 前失败时 tmp 可能存在) - try: - os.remove(tmp) - except FileNotFoundError: - pass # rename 已成功或 tmp 未创建 - raise e # 向上层返回错误 -``` - -**initializeMetadata — 异常分析** - -| 阶段 | 异常类型 | 处理方式 | -|------|---------|---------| -| `mkdir(.zstack-vm-metadata)` 失败 | 磁盘满、权限 | Agent 返回错误 → 控制面标记 VM 元数据初始化失败 → `markDirty` 后由 Poller 重试 | -| `mkdir` 成功但 `chmod 0700` 失败 | NFS 权限异常 | 已创建目录可能权限不正确 → Agent 返回错误。下次重试时 `mkdir(exist_ok=True)` + 重新 `chmod` | -| 写入 payload 失败 | 同 writeMetadata 各阶段异常 | 同 writeMetadata 处理。目录已创建但文件不存在 → 下次 `initializeMetadata` 或 Poller `writeMetadata` 时创建 | - -`initializeMetadata` 使用与 `writeMetadata` 相同的 tmp+rename 原子路径,因此文件层面的异常处理完全一致。额外关注点仅在目录创建阶段。 - -**readMetadata — 异常分析** - -| 异常类型 | 处理方式 | -|---------|---------| -| 文件不存在(`FileNotFoundError`) | 返回 `null` → 调用方判断:可能是 VM 新创建尚未初始化,或文件被误删 | -| 文件存在但读取失败(`IOError`) | 返回错误 → 调用方按失败处理(重试或告警) | -| 文件内容非法 JSON(`json.loads()` 异常) | 视为损坏 → 返回错误 → 控制面 `markDirty()` → 下轮 Poller 从 DB 全量重建覆盖写入 | -| NFS stale file handle | Agent 返回错误 → Poller 重试(NFS 重连后恢复) | -| `.sc.tmp` 残留文件检测(讨论补充) | `readMetadata` 读取 `.json` 文件时,同步检查同目录是否存在 `{vm_uuid}.json.sc.tmp` 文件。若存在,说明存储迁移写入未完成(write-tmp 成功但 rename 前崩溃),在返回结果中标记 `storageChangeIncomplete=true`,注册端据此拒绝注册(readStatus = `STORAGE_CHANGE_INCOMPLETE`)。普通 `.tmp` 残留不影响 readStatus(仅代表普通写入中断,`.json` 文件本身仍为上次成功写入的完整版本) | - -**deleteMetadata — 异常分析** - -| 异常类型 | 处理方式 | -|---------|---------| -| 文件不存在(`FileNotFoundError`) | **视为成功**(C-01C-9 幂等约束) | -| 删除失败(`IOError`/权限) | Agent 返回错误 → 控制面同步重试(3 次指数退避:30s/60s/120s)→ 仍失败则残留为孤儿文件,由 Part 2b §8.3 巡检清理。注:VM 删除后 FK CASCADE 已清除 dirty 行,Poller 无法介入 | -| 同时删除 `.tmp`/`.sc.tmp` 残留 | `deleteMetadata` 除了删除 `.json` 文件外,还应尝试删除同名的 `.json.tmp` 和 `.json.sc.tmp`(如存在),避免孤儿 tmp 残留。删除 tmp 失败不影响主操作成功 | - -**NFS 特有异常场景** - -| 场景 | 表现 | 影响 | 处理 | -|------|------|------|------| -| **NFS server 宕机** | 所有文件操作阻塞/超时返回 `EIO` | Poller flush 全部失败 | 指数退避重试。NFS 恢复后自动恢复正常。不影响 VM 运行 | -| **NFS client 端缓存过期** | `readMetadata` 可能读到旧版本 | 扫描/注册场景可能看到过期数据 | 可接受:注册场景会做额外校验;Poller 下轮 flush 覆盖 | -| **NFS mount 断开(`ESTALE`)** | 文件操作返回 `errno=116 ESTALE` | 同 NFS server 宕机 | 同上。Agent 应捕获 `ESTALE` 并返回可重试错误码 | -| **多 Host 并发操作同一 `.json`** | 理论上不会发生(Poller CAS 保证单 MN 持有) | — | 防御性措施:若检测到文件被意外修改(mtime 变化),日志告警但不中断写入 | - -### 1.3 MetadataStorageHandler 接口 - -不同存储类型的元数据读写操作通过统一接口抽象: - -```java -public interface MetadataStorageHandler { - void initializeMetadata(String psUuid, String vmUuid, String payloadJson, Completion completion); - void deleteMetadata(String psUuid, String vmUuid, Completion completion); - void writeMetadata(String psUuid, String vmUuid, String payloadJson, - boolean storageStructureChange, Completion completion); - void readMetadata(String psUuid, String vmUuid, ReturnValueCompletion completion); - boolean isMetadataSupported(String psType); - - /** - * 扫描指定 PS 上所有元数据条目,返回 VmMetadataEntry 列表(轻量级,不读取 payload)。 - * sblk: 扫描 VG 中所有 *_vmmeta LV,提取 vmUuid 前缀 - * local/NFS: 列举 .zstack-vm-metadata/ 目录下 *.json 文件名 - * 用途: MetadataOrphanDetector (Part 2b §8.4.2)、Scan API (Part 5 §2) - * - * 返回类型变更说明(讨论 Δ-7):原方案返回 List(纯 vmUuid), - * 改为返回 List,其中 VmMetadataEntry 包含: - * - vmUuid: String — 虚拟机 UUID - * - hostUuid: String — 对于 Local Storage,标识元数据文件所在 Host; - * 对于 SharedBlock/NFS 等共享存储,hostUuid 可为 null。 - * 原因:Local Storage 场景下扫描需要逐 Host 执行,调用方需要知道元数据 - * 位于哪台 Host 上以便后续操作(如孤儿清理、注册时路由)。若仅返回 vmUuid, - * 调用方无法区分同一 PS 不同 Host 上的元数据条目。 - */ - void scanMetadataVmUuids(String psUuid, ReturnValueCompletion> completion); - - /** - * 元数据扫描结果条目。 - */ - class VmMetadataEntry { - private String vmUuid; - private String hostUuid; // nullable: SharedBlock/NFS 场景为 null - } -} -``` - -**重要设计约束**:Agent 端不解析 DTO 内容。控制面负责 DTO 的构建、序列化和反序列化。Agent 只负责将 payload 原样写入/读取。 - -| 实现类 | 存储类型 | initializeMetadata | writeMetadata | readMetadata | deleteMetadata | -|--------|---------|-------------------|---------------|--------------|----------------| -| `SblkMetadataStorageHandler` | SharedBlock | 创建 LV + 写 Header + 写入完整 payload | 三阶段原子写入 LV | 读 Header + Active Slot | `lv_delete` | -| `LocalNfsMetadataStorageHandler` | Local/NFS | 创建文件 + 写入完整 payload | tmp(区分 `.tmp`/`.sc.tmp`)+ fsync + rename | 读 JSON(解析失败视为损坏) | `os.remove()` | - -**Handler 动态路由**(SM-07 修复):`MetadataStorageHandler` 通过 `psUuid` 参数动态路由——每次调用时根据 `PrimaryStorageVO.type` 查找对应 Handler 实现,支持同一迁移流程中源/目标使用不同 Handler。例如 VM 从 SharedBlock 迁移到 NFS 时,Step 4 `initializeMetadata(targetPsUuid)` 路由到 `LocalNfsMetadataStorageHandler`,Step 7 `deleteMetadata(sourcePsUuid)` 路由到 `SblkMetadataStorageHandler`。 - -### 1.4 元数据生命周期 - -| 事件 | 行为 | -|------|------| -| 新创建虚拟机 | 自动创建元数据文件 | - -**VM 创建失败时的元数据清理**:`APICreateVmInstanceMsg` 的 FlowChain 在末尾 Flow 调用 `initializeMetadata` + `markDirty`。若 FlowChain 中更早的 Flow(如分配 IP、创建磁盘)失败,FlowChain 的 rollback 机制会回退所有已完成 Flow(包括 VmInstanceVO 本身通过 `VmAllocateVolumeFlow.rollback` 等清理)。由于 `initializeMetadata` Flow 尚未执行,存储侧不存在元数据文件,无需清理。若 `initializeMetadata` 本身执行成功但后续 Flow 失败(极端场景),`VmCreationRollbackFlow` 应包含 `deleteMetadata` 调用清理残留。若 `initializeMetadata` 执行失败,FlowChain rollback 删除所有已创建 VO,FK CASCADE 清理 dirty 行(如有),孤儿文件由 Part 2b §8.4 巡检兜底。 - -| VM 删除 | 同步删除元数据文件;删除失败时同步重试(3 次指数退避),仍失败 → 孤儿 LV/文件残留 → 由健康巡检([Part 2b §8.3](vm-metadata-02b-高可用与运维.md#83-vm-销毁时的元数据清理))兜底清理。注意:VM 删除后 FK CASCADE 已删除 `VmMetadataDirtyVO` 行,Poller 无法介入,因此使用同步重试 | - -**元数据删除时机(讨论 Δ-5)**:元数据文件的删除发生在 **ExpungeVmInstanceFlow**(物理删除阶段),而非 DestroyVmInstanceFlow(软删除阶段)。原因: -1. DestroyVm 仅执行软删除(`VmInstanceVO` → `VmInstanceEO`),VM 可通过 `APIRecoverVmInstanceMsg` 恢复。若在 Destroy 时删除元数据,Recover 后元数据丢失且无法自动恢复(需手动触发全量刷写)。 -2. Expunge 是不可逆的物理清除,此时删除元数据是安全的(VM 不可能再恢复)。 -3. Destroyed 状态的 VM 已被 Poller 的前置检查过滤(Part 2 §4.4),不会执行无效刷写。 -4. Destroy → Expunge 窗口内元数据保留不影响存储空间(元数据文件通常 <500KB)。 - -**deleteMetadata 重试参数可配**:当前硬编码 3 次重试(30s/60s/120s)。改为通过 GlobalConfig 配置:`vm.metadata.delete.maxRetry`(默认 3)、`vm.metadata.delete.baseDelaySec`(默认 30)。计算方式:`baseDelay × 2^(retryIndex)`,与 Poller 退避公式一致。此配置项添加到 Part 2b §13 GlobalConfig 汇总表。 - -| 存储迁移 | 暂停 Poller → 数据迁移 → DB 更新 → 目标端初始化写入与校验 → 恢复 Poller + markDirty → 源端清理 | -| 不支持的存储类型 | 静默跳过,不创建元数据文件 | - -**存储迁移场景分类**(SM-08 修复): - -| 场景 | 条件 | 元数据处理 | -|------|------|-----------| -| **(A) 整 VM 存储迁移** | Root Volume 参与迁移(含或不含 DataVolume) | 执行完整 7-step 流程:暂停 Poller → 数据迁移 → DB 更新 → 目标端初始化写入 + read-back 校验 → 恢复 Poller → 源端清理 | -| **(B) 单 DataVolume 迁移** | 仅 DataVolume 迁移,Root Volume 不动 | 元数据锚定在 Root PS,**无需**暂停 Poller / initializeMetadata / deleteMetadata。迁移完成后仅需 `markDirty(vmUuid, true)` 触发 Poller 重写(因 `VolumeVO.installPath` 已变更,payload 需更新) | - -以下 7-step 流程仅适用于场景 (A)。场景 (B) 的判断依据:迁移的卷列表中不包含 `VolumeVO.type = Root` 的卷。 - -**存储迁移时的元数据生命周期**(强一致路径,失败阻断源端清理): - -``` -Step 1: 暂停该 VM 的 Poller flush - INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) VALUES (:vmUuid, 1, 1) - UPDATE VmMetadataDirtyVO SET nextRetryTime='2099-12-31 23:59:59' WHERE vmInstanceUuid=:vmUuid -``` - -**Step 1 INSERT IGNORE 说明(讨论 Δ-6)**:原方案直接 UPDATE `nextRetryTime`,但若该 VM 当前无 dirty 行(已被 Poller 成功处理后删除),UPDATE 将匹配 0 行,后续 Step 6 恢复 Poller 时也无行可操作。改为先 `INSERT IGNORE`(确保 dirty 行存在)再 `UPDATE`(设定暂停哨兵值),与 `markDirty()` 的两步语义保持一致。`storageStructureChange=1` 因为存储迁移必然涉及存储拓扑变更。 -Step 2: 数据迁移(卷/快照) -Step 3: DB 更新(VolumeVO.primaryStorageUuid/installPath) -Step 4: initializeMetadataOnTargetPS(vmUuid, targetPsUuid) - # DB 已指向目标端,payload 基于最新 DB 构建,installPath 已是目标端路径。 - # 同时完成容器创建(sblk LV / NFS 目录)和正确 payload 写入,无 stale 数据。 - # SM-03 交叉引用:storageStructureChange=true → OP type=2 STORAGE_CHANGE,参见 Part 4c §3 -Step 5: readMetadata(targetPsUuid, vmUuid) + readStatus 校验 + JSON 可解析性验证(参见 D-1c-1,不设 checksum) -Step 6: 恢复 Poller(nextRetryTime=NULL)并 markDirty(vmUuid, true) -Step 7: deleteMetadataOnSourcePS(vmUuid, sourcePsUuid) -``` - -Step 5 为必选保护:在源端清理前,目标端必须已有完整元数据,禁止仅依赖异步 Poller 首刷。 - -**为什么 initializeMetadata 在 DB 更新之后执行**:在 §1.4 流程中,`initializeMetadataOnTargetPS` 被安排在 Step 4(DB 更新之后),而非数据迁移之前。这一设计消除了 stale payload 问题——payload 基于最新 DB 构建,`installPath` 已指向目标端,无需额外覆盖写入。若采用旧方案(在数据迁移前预创建元数据),payload 中的 `installPath` 仍指向源 PS,需要后续步骤覆盖,且在 MN 崩溃时会产生内容过期的孤儿元数据。当前方案的优势:(1) 一次写入即正确,无 stale 数据;(2) 缩小孤儿窗口——仅 Step 4 成功后、Step 5~6 失败时才需要 SM-01 回滚清理目标端残留;(3) sblk 场景下无需先创建空 LV 再覆盖。 - -**Step 6 `markDirty` 防御性设计理由**(SM-05 修复):Step 4 已同步写入完整元数据到目标端,Step 6 的 `markDirty(vmUuid, true)` 看似冗余,但作为防御性措施是必要的——Step 2~5 执行期间可能有其他 API 修改 VM 配置(如热插拔网卡、修改 HA 级别),导致 Step 4 同步写入的内容与最新 DB 状态存在微小时间差。`markDirty` 确保 Poller 异步刷写能将目标端元数据收敛到最终一致状态。 - -**迁移清理前 double-check(Root Volume)**: - -```java -String rootPsUuid = Q.New(VolumeVO.class) - .eq(VolumeVO_.vmInstanceUuid, vmUuid) - .eq(VolumeVO_.type, VolumeType.Root) - .select(VolumeVO_.primaryStorageUuid) - .findValue(); -if (oldPsUuid.equals(rootPsUuid)) { - logger.warn("VM {} root volume still on source PS {}, skip metadata cleanup", vmUuid, oldPsUuid); - return; -} -String targetPayload = metadataStorageHandler.readMetadata(targetPsUuid, vmUuid); -if (targetPayload == null || targetPayload.trim().length() <= 2) { // 防御性检查:异常场景下可能出现空文件或损坏文件 - logger.warn("VM {} target metadata is empty, skip source cleanup", vmUuid); - return; -} -metadataStorageHandler.deleteMetadata(oldPsUuid, vmUuid, ...); -``` - -元数据锚定在根盘所在 PS,因此 double-check **仅需校验根盘**的 `primaryStorageUuid` 是否仍在源 PS 上。DataVolume 的位置不影响元数据存储位置。 - -**C-01C-9**(约束):`deleteMetadata` 必须幂等——删除不存在的元数据(LV 已删除或 JSON 文件不存在)必须返回成功(不抛异常)。`SblkMetadataStorageHandler.deleteMetadata` 中 `lv_delete` 对不存在的 LV 应返回 0(非错误);`LocalNfsMetadataStorageHandler.deleteMetadata` 中 `os.remove()` 应捕获 `FileNotFoundError` 并视为成功。 - -**失败回滚策略**:Step 2-6 任一步失败,必须执行 `nextRetryTime=NULL` 恢复 Poller,且不得执行 Step 7。后续通过 `markDirty(vmUuid, false)` 回到源路径刷写。 - -**SM-01 修复:Step 4 成功后的目标端清理**:若 Step 4 `initializeMetadataOnTargetPS` 已成功(目标端 LV 或 JSON 文件已创建并写入 payload),Step 5~6 任一步失败时,回滚必须先执行 `deleteMetadata(targetPsUuid, vmUuid)` 清理目标端残留,再恢复 Poller。不清理会导致:(1) 后续重试时 `initializeMetadata` 报"已存在"错误;(2) 目标端残留成为孤儿资源。回滚顺序:`deleteMetadata(target)` → `nextRetryTime=NULL` → `markDirty(vmUuid, false)`。 - -**flush 路径解析策略**(关联 [Part 2b §8.2](vm-metadata-02b-高可用与运维.md#82-路径指纹巡检--轻量级漂移检测)): - -- `doFlush()` 每次都从当前 `VolumeVO.installPath/primaryStorageUuid` 动态解析目标 PS -- 禁止缓存上一次 flush 的 psUuid/path -- 迁移回滚后下一轮 flush 自动回到源 PS(对应 Q2b-7 修复点) - -**存储迁移时序分析**(QX-1 全链路一致性): - -``` -T1: pause poller(nextRetryTime=FAR_FUTURE) — 对应 Step 1 -T2: 数据搬迁(卷/快照) — 对应 Step 2 -T3: DB installPath/psUuid 切换为 target — 对应 Step 3 -T4: initializeMetadataOnTargetPS(vmUuid, targetPsUuid) — 对应 Step 4 - # DB 已指向目标端,payload 基于最新 DB 构建,installPath 已是目标端路径。 - # 同时完成容器创建(sblk LV / NFS 目录)和正确 payload 写入,无 stale 数据。 - # SM-02 崩溃窗口:T4→T5 之间若 MN 崩溃,目标端已创建元数据但尚未校验。 - # DB 已指向目标端,§1.6 崩溃恢复重置 nextRetryTime 后,Poller 基于当前 DB - # 写入目标端,自动收敛到正确状态。无孤儿风险(DB 与元数据位置一致)。 -T5: readMetadata + read-back verify + JSON 可解析性验证 — 对应 Step 5 -T6: resume poller(nextRetryTime=NULL) + markDirty(vmUuid, true) — 对应 Step 6 -T7: root/data volume 双重校验 + target 非空校验 → deleteMetadata — 对应 Step 7 -``` - -**关键保证**:源端清理前已经完成目标端同步写入和 read-back 校验;Poller 仅作为后续收敛机制,而非迁移正确性的前置条件。 - -**与旧方案的差异**:早期设计中 `initializeMetadataOnTargetPS` 在数据迁移之前执行(预创建),此时 DB 尚未切换,写入的 payload 包含源端 `installPath`(stale 数据),需后续覆盖。现已调整为 Step 4(DB 更新之后),消除 stale payload 问题,并缩小了 SM-02 崩溃窗口的影响——崩溃后 Poller 恢复即可写入正确的目标端数据,无需孤儿巡检兜底。 - -### 1.5 不支持的存储类型 - -| 场景 | 行为 | -|------|------| -| VM 根盘在不支持的存储上 | 静默跳过,不创建元数据文件 | -| `@MetadataImpact` 拦截器触发时 | 检查根盘存储类型,不支持的直接跳过 markDirty | -| 注册 API 指定不支持的存储 | 返回错误 `METADATA_STORAGE_NOT_SUPPORTED` | - -**Local Storage + VM 热迁移(非存储迁移)的元数据处理**:VM 热迁移(`APIMigrateVmMsg`)仅迁移 VM 进程,不移动磁盘数据。Local Storage 场景下,VM 热迁移**不支持**(ZStack 约束:Local Storage 的 VM 不允许热迁移,仅允许存储迁移)。因此不存在"VM 迁移到另一 Host 但元数据文件在源 Host Local 磁盘上"的场景。SharedBlock/NFS 场景下热迁移不影响元数据位置(通过共享存储访问)。此场景无需额外处理。 - -### 1.6 存储迁移 Poller 暂停的崩溃恢复(H3 修复) - -**问题**:§1.4 Step 1 将 `nextRetryTime` 设为 `'2099-12-31 23:59:59'` 暂停 Poller,Step 6 恢复为 NULL。若 MN 在 Step 1 之后、Step 6 之前崩溃(或迁移流程异常退出未触发失败回滚),该 dirty 行的 `nextRetryTime` 将永久停留在远未来值,导致该 VM 的元数据刷写被永久阻塞。 - -**修复方案 — MN 启动扫描 + 自动重置**: - -在 `managementNodeReady()` 回调中,Poller 启动前执行一次性扫描: - -```java -/** - * 崩溃恢复:检测并重置被迁移暂停但未恢复的 dirty 行。 - * 判断条件:nextRetryTime > NOW() + 1 hour(正常退避最大值远小于此) - * 安全性:若迁移确实仍在进行(MN 未崩溃),该行的 managementNodeUuid 不为 NULL, - * 不会被 Poller 认领;此处仅重置 nextRetryTime 和 retryCount,不修改认领状态。 - */ -private void recoverStalledMigrationPauses() { - // DP-10 修复:改为精确匹配迁移暂停哨兵值 '2099-12-31 23:59:59', - // 避免误重置正常指数退避的行(最大退避约 2.8h,远小于此阈值)。 - // 原代码使用 `> TIMESTAMPADD(HOUR, 1, CURRENT_TIMESTAMP)` 存在误重置风险。 - int recovered = SQL.New( - "UPDATE VmMetadataDirtyVO " + - "SET nextRetryTime = NULL, retryCount = 0 " + // SM-09 修复:同时重置 retryCount,给予完整重试配额 - "WHERE nextRetryTime = '2099-12-31 23:59:59'") - .execute(); - if (recovered > 0) { - logger.warn("Recovered {} dirty rows with stalled migration pause (nextRetryTime far in future)", recovered); - } -} -``` - -**调用时机**:`managementNodeReady()` 中,先调用 `recoverStalledMigrationPauses()`,再启动 Poller(`thdf.submitPeriodicTask()`)。 - -**安全性分析**: - -| 场景 | 行为 | 安全性 | -|------|------|--------| -| MN 崩溃后重启,迁移已失败 | `nextRetryTime` 被重置为 NULL → Poller 正常认领 → 从 DB 全量重建 | (Y) 安全(DB 已反映回滚后状态) | -| MN 崩溃后重启,迁移已成功(Step 4 DB 已切换) | 同上,flush 到新 PS | (Y) 安全(DB installPath 已指向目标) | -| 双 MN 场景,另一 MN 正在执行迁移 | dirty 行 `managementNodeUuid` 不为 NULL → Poller CAS 条件排除 → 不会重复处理 | (Y) 安全(仅重置时间,不抢认领) | -| 正常退避中的 dirty 行(retryCount < max) | 退避最大值 = `baseDelay × 2^maxExponent`(默认 10 × 1024 ≈ 10240s ≈ 2.8h) | (Y) 安全(精确匹配哨兵值,不会误重置正常退避行) | - -**已采用精确匹配**(DP-10 修复):使用 `nextRetryTime = '2099-12-31 23:59:59'` 而非 `> NOW() + 1h`,完全消除误重置正常退避行的风险。最终 SQL: - -```sql -UPDATE VmMetadataDirtyVO SET nextRetryTime = NULL, retryCount = 0 -WHERE nextRetryTime = '2099-12-31 23:59:59' -``` - -**与 §1.4 失败回滚的关系**:§1.4 的失败回滚策略("Step 2-6 任一步失败,必须执行 `nextRetryTime=NULL` 恢复 Poller")覆盖了**正常失败**场景。本节 H3 修复覆盖的是**异常退出**场景(MN 崩溃、JVM OOM、进程被 kill 等导致回滚逻辑未执行)。两者互补,无冲突。 - ---- - -## 2. 模板虚拟机与链式克隆元数据 - -### 2.1 模板 VM 数据模型概述 - -``` -VmInstanceVO (type = "UserVm", 模板 VM) - │ uuid (1:1, CASCADE) - ▼ -TemplatedVmInstanceVO ← 纯标记表 - ├── TemplatedVmInstanceCacheVO ← 缓存 VM - │ └── cacheVmInstanceUuid → VmInstanceVO (缓存 VM) - │ └── VolumeVO → VolumeSnapshotVO - │ └── VolumeSnapshotReferenceVO ← 子 VM 的引用记录 - └── TemplatedVmInstanceRefVO ← 子 VM 追溯 - └── vmInstanceUuid → VmInstanceVO (子 VM) -``` - -### 2.2 元数据策略 - -#### 模板 VM(vmCategory = TEMPLATE) - -写入元数据,注册时作为普通 VM 恢复(不恢复模板身份)。 - -模板 VM 的元数据存储位置与普通 VM 一致:**以 RootVolume 所在 Primary Storage 为唯一存储锚点**,不使用 `TemplatedVmInstanceCacheVO` 的缓存卷位置作为元数据路径来源。 - -**不纳入元数据的关联表**: - -| VO | 理由 | -|----|------| -| `TemplatedVmInstanceVO` | 纯标记表无业务字段,`vmCategory=TEMPLATE` 已标记身份 | -| `TemplatedVmInstanceCacheVO` | 缓存 VM 是运行态产物,跨环境无意义 | -| `TemplatedVmInstanceRefVO` | 子 VM 追溯关系属于旧环境 | - -#### 缓存 VM(vmCategory = TEMPLATE_CACHE) - -**写入元数据**(供扫描展示),但**拒绝注册**。 - -- 写入理由:扫描结果中 admin 可识别缓存 VM 身份 -- 不注册理由:缓存 VM 是内部运行态资源,新环境自动创建 - -#### 子 VM / 链式克隆(vmCategory = REGULAR) - -作为普通 VM 注册,额外恢复 `VolumeSnapshotReferenceTreeVO` 和 `VolumeSnapshotReferenceVO`。注册后等效于**模板和缓存已被删除**的状态。 - -### 2.3 VolumeSnapshotReferenceVO/TreeVO 的 FK 约束分析 - -权威 FK 定义见 [Part 1a §2.4](vm-metadata-01a-数据模型与序列化.md#24-volumeresourcemetadata)。 - -**DDL 层面 FK 约束摘要**: - -| 表 | 字段 | FK 目标 | ON DELETE | 注册安全性 | -|----|------|---------|-----------|------------| -| `VolumeSnapshotReferenceTreeVO` | 所有字段 | **无 FK 约束** | — | (Y) 可直接插入,`rootVolumeSnapshotUuid`/`rootVolumeUuid`/`hostUuid` 等均为逻辑引用 | -| `VolumeSnapshotReferenceVO` | `referenceVolumeUuid` | `VolumeEO.uuid` | CASCADE | (Y) 指向子 VM 的卷,注册时先创建 VolumeVO 即可满足 | -| `VolumeSnapshotReferenceVO` | `treeUuid` | `VolumeSnapshotReferenceTreeVO.uuid` | SET NULL | (Y) 先插入 TreeVO 即可满足 | -| `VolumeSnapshotReferenceVO` | `parentId` | 自引用 `VolumeSnapshotReferenceVO.id` | SET NULL | (Y) 按层级顺序插入 | -| `VolumeSnapshotReferenceVO` | `volumeUuid`, `volumeSnapshotUuid`, `directSnapshotUuid`, `referenceUuid` | **无 FK 约束** | — | (Y) 可引用旧环境不存在的 UUID(逻辑引用) | - -**关键结论**:`VolumeSnapshotReferenceTreeVO.rootVolumeSnapshotUuid` 无 FK 到 `VolumeSnapshotVO`,因此注册子 VM 时即使缓存 VM 的快照不存在于新环境,TreeVO 插入也不会违反约束。注册顺序:`VolumeVO`(子 VM 卷)→ `VolumeSnapshotReferenceTreeVO` → `VolumeSnapshotReferenceVO`。 - -子 VM 的 Reference 记录在缓存 VM 被删除后仍然安全可用。代码层面验证(`VolumeSnapshotReferenceUtils.java`): - -| 操作场景 | 是否需要缓存 VM 的 VolumeSnapshotVO | 原因 | -|---------|:---:|------| -| 删除子 VM 卷 | 否 | `backingVolumeDeletedInDb=true` 时直接走 `deleteBitsOnPs` | -| Flatten 子 VM(无快照) | 否 | `referenceType=VolumeVO` → 直接删 ref | -| Flatten 子 VM(有快照) | 仅子 VM 自己的 | `ref.getReferenceUuid()` 查子 VM 快照 | -| 子 VM 创建快照 | 否 | 仅更新 ref 字段 | -| 子 VM 删除快照 | 仅子 VM 自己的 | 查询条件限制为子 VM 快照 UUID | -| 子 VM 卷路径变更 | 否 | 仅更新 `referenceInstallUrl` | - -### 2.4 模板相关 API 的 @MetadataImpact - -| 操作 | @MetadataImpact | vmCategory 变化 | -|------|----------------|-----------------| -| 普通 VM 转模板 VM | `CONFIG` | REGULAR → TEMPLATE | -| 模板 VM 转回普通 VM | `CONFIG` | TEMPLATE → REGULAR | -| 从模板创建子 VM(首次) | 不影响模板本身 | 自动创建 TEMPLATE_CACHE | -| 更新模板 VM 属性 | `CONFIG` | 不变 | - ---- - -## 3. 约束与不変量 - -| 约束 ID | 内容 | 来源章节 | -|---------|------|----------| -| C-01C-2 | sblk LV 名称使用 `{vm_uuid}_vmmeta`,长度计算为 39,必须始终小于 LVM 128 上限 | §1.1, §1.2 | -| C-01C-3 | 模板 VM(TEMPLATE)元数据写入位置锚定 RootVolume 所在 PS,不依赖 cache VM 路径 | §2.2 | -| C-01C-4 | 存储迁移必须在源端清理前完成目标端同步写入与 read-back 校验;禁止仅依赖异步 Poller 首次刷写 | §1.4 | -| C-01C-5 | 存储迁移清理必须校验根盘的 `primaryStorageUuid` 是否仍在源 PS,若根盘仍在源 PS 则不得 deleteMetadata(source) | §1.4 | -| C-01C-6 | flush 路径必须按 `VolumeVO.installPath/primaryStorageUuid` 动态解析,不得缓存历史路径 | §1.4 | -| C-01C-7 | 迁移期间对 dirty 行 `nextRetryTime` 的暂停/恢复必须成对出现;失败回滚时必须恢复 Poller | §1.4 | -| C-01C-8 | MN 启动时必须扫描并重置 `nextRetryTime='2099-12-31 23:59:59'` 的迁移暂停行;该扫描必须在 Poller 启动之前执行 | §1.6 | -| C-01C-9 | `deleteMetadata` 必须幂等——删除不存在的元数据必须返回成功(不抛异常) | §1.4 | -| C-01C-10 | local/NFS 的 tmp 文件使用固定命名(`.tmp`/`.sc.tmp`),Agent 启动时扫描清理 `*.tmp` 残留 | §1.2 | -| C-01C-11 | `MetadataStorageHandler` 接口必须包含 `scanMetadataVmUuids()` 方法,用于孤儿检测和 Scan API | §1.3 | -| C-01C-12 | `deleteMetadata` 重试参数(次数、退避基础延迟)必须通过 GlobalConfig 配置,不得硬编码 | §1.4 | diff --git "a/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" "b/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" deleted file mode 100644 index dfcb00afd92..00000000000 --- "a/docs/design/vm-metadata-02-\350\204\217\346\240\207\350\256\260\344\270\216Poller.md" +++ /dev/null @@ -1,939 +0,0 @@ -# VM 元数据 — 脏标记与 Poller - -## 目录 - -1. [概述](#1-概述) -2. [数据模型](#2-数据模型) -3. [markDirty — 标脏入口](#3-markdirty--标脏入口) -4. [MetadataDirtyPoller — 轮询刷写](#4-metadatadirtypoller--轮询刷写) - - [4.8 Stale 恢复任务](#48-stale-恢复任务h2-修复) -5. [消息调用链](#5-消息调用链) -6. [并发控制(四层)](#6-并发控制四层) - - [6.4 调优指南](#64-调优指南) -7. [约束与不変量](#7-约束与不変量) - ---- - -# 1. 概述 - -## 1.1 问题:GC 框架的结构性错配 - -GC 框架是 **"一个任务对应一行 DB 记录"** 的模型。每次 API 成功都 `submit()` 创建新 GC 行,通过 ChainTask `maxPendingTasks=1` + `exceedMaxPendingCallback` 淘汰多余行。这导致: - -| 问题 | 说明 | -|------|------| -| **GC 行爆炸** | 100 个 API → 100 行 GC,98 行立即 Done,需定期清理 | -| **deduplicateSubmit 不可用** | GC 执行期间 status 仍为 Idle,新 GC 被误判"已有在处理" | -| **双 MN 复杂度** | 需 hash 环路由 SubmitGCMsg + 执行层 delegation + reply 回退,6 种极端情况需逐一分析 | -| **delegation 消耗 retryCount** | 非 owner 上 triggerNow 的 delegation 失败也递增 retryCount | -| **框架修改** | 需修改 loadOrphanJobs 增加状态过滤、需新增索引 | - -**根因**:元数据更新需要的是 **"标脏 → 合并 → 刷写"** 模型(多次修改合并为一次写入),而非 GC 的 **"一个失败任务 → 一次重试"** 模型。 - -## 1.2 新方案一句话 - -用一张 **`VmMetadataDirtyVO`** 表做脏标记(一个 VM 最多一行),**`PeriodicTask`** 轮询器定期认领并刷写,成功删行,失败释放等下轮。 - -灵感来源:`SecurityGroupFailureHostVO` + `FailureHostWorker` 模式。 - -## 1.3 核心不变量 - -- 刷写时始终从 DB 查询 VM 完整当前状态构建 payload,不使用触发 API 时的增量数据。 -- 任何一次刷写完成后,存储上的元数据反映数据库最新完整状态。 -- `buildVmInstanceMetadata()` 必须标注 `@Transactional(readOnly = true)`,MySQL InnoDB REPEATABLE READ 事务内所有查询使用同一快照,保证单次构建的读一致性。`readOnly = true` 不启动写事务,开销极小。 - -## 1.4 最终一致性模型 - -`buildVmInstanceMetadata()` 读 DB 到 `pwrite` 完成之间存在毫秒级窗口,期间其他 API 可能修改了 DB(如删除快照)。此时写入的元数据可能包含已过期信息。这不是问题——修改 DB 的 API 成功后会再次 `markDirty()`,下轮 Poller 从 DB 全量读取已反映最新状态,覆盖写入自然修正。 - -对注册场景,Part 3 §3.4 的 installPath 存在性检查提供额外兜底。 - ---- - -# 2. 数据模型 - -## 2.1 VmMetadataDirtyVO - -```java -@Entity -@Table -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; // null = 未认领,非null = 已认领 - - @Column - private Timestamp lastClaimTime; // 最近一次被 CAS 认领的时间(死锁防护) - - @Column - private long dirtyVersion; // 每次 markDirty 递增,用于检测刷写期间的新变更 - - @Column - private boolean storageStructureChange; // 是否涉及存储结构变更(OP type 标记) - - @Column - private int retryCount; // 连续失败次数 - - @Column - private Timestamp nextRetryTime; // 下次可被认领的时间(退避控制) - - @Column - private Timestamp createDate; - - @Column - private Timestamp lastOpDate; // 最后一次 markDirty 的时间(关键!) -} -``` - -**关键设计决策**: - -| 设计点 | 决策 | 原因 | -|--------|------|------| -| `vmInstanceUuid` 做主键 | 一个 VM 最多一行 | 天然去重,100 个 API 只产生 1 行,不是 100 行 | -| `managementNodeUuid` FK SET_NULL | MN 宕机自动释放 | 无需额外孤儿扫描,DB 约束自动完成 | -| `lastClaimTime` | claim 存活时间上限控制 | 识别僵尸 claim,支持 stale 认领接管 | -| `vmInstanceUuid` FK CASCADE | VM 销毁自动删除脏标记 | 无残留 | -| `dirtyVersion` | 每次 markDirty +1 | 刷写前快照 version,成功后比较——检测刷写期间是否有新变更(见 §4.5)。语义比时间戳比较更明确,无精度问题 | - -**`dirtyVersion` per-row 语义澄清**:`dirtyVersion` 是每行独立的单调递增计数器(从 1 开始),不是全局序列号。其用途仅限于同一 VM 的 `onFlushSuccess` 版本比较(检测刷写期间是否有新 markDirty),不用于跨 VM 排序。跨 VM 公平调度使用 `lastOpDate`。BIGINT 范围(9.2×10^18)足够单 VM 终身使用(假设 1000 次/秒,需 2.9 亿年溢出),无需溢出保护。 -| `storageStructureChange` | OR 升级策略 | `@MetadataImpact(CONFIG)` → false(OP type 1),`@MetadataImpact(STORAGE)` → true(OP type 2)。多次 markDirty 取 OR:一旦标记为 STORAGE 则本轮不降级 | -| `lastOpDate` | MySQL 自动更新 | Poller 认领时排序依据(最早变更优先处理) | -| `nextRetryTime` | 退避控制 | 失败后不立刻重试,等到下次重试时间 | - -## 2.2 DDL - -```sql -CREATE TABLE VmMetadataDirtyVO ( - vmInstanceUuid VARCHAR(32) NOT NULL, - managementNodeUuid VARCHAR(32) DEFAULT NULL, - lastClaimTime TIMESTAMP NULL DEFAULT NULL, - dirtyVersion BIGINT NOT NULL DEFAULT 1, - storageStructureChange TINYINT(1) NOT NULL DEFAULT 0, - retryCount INT NOT NULL DEFAULT 0, - nextRetryTime TIMESTAMP NULL DEFAULT NULL, - createDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - lastOpDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - 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; -``` - -## 2.3 推荐索引 - -```sql --- Poller CAS 认领查询: WHERE managementNodeUuid IS NULL AND nextRetryTime <= NOW() -CREATE INDEX idx_dirty_unclaimed ON VmMetadataDirtyVO (managementNodeUuid, lastClaimTime, nextRetryTime); -``` - -**约束**:`lastClaimTime` 允许为空(历史数据兼容);新版本 claim 路径必须在 CAS 成功时写入当前时间。 - -与 GarbageCollectorVO 的详细对比见 [对比文档 §1](2/vm-metadata-new-02h-compare.md#1-数据模型对比vmmetadatadirtyvo-vs-garbagecollectorvo)。 - ---- - -# 3. markDirty — 标脏入口 - -## 3.1 核心逻辑 - -```java -public boolean markDirty(String vmInstanceUuid, boolean storageStructureChange) { - // Q23 修复:返回 boolean 表示标脏是否成功(供 MetadataStaleRecoveryTask DP-03 使用) - // 前置检查:功能开关 - if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { - return false; - } - - // Q2-2: Galera 集群兼容写法,避免 INSERT ON DUPLICATE KEY 在高并发下死锁 - // Step 1: INSERT IGNORE(新行) - int inserted = SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + - "VALUES (:vmUuid, 1, :ssc)") - .param("vmUuid", vmInstanceUuid) - .param("ssc", storageStructureChange) - .execute(); - - // Step 2: 仅在行已存在时执行 UPDATE(dirtyVersion +1, storageStructureChange OR 升级) - if (inserted == 0) { - int updated = SQL.New("UPDATE VmMetadataDirtyVO " + - "SET dirtyVersion = dirtyVersion + 1, " + - " storageStructureChange = storageStructureChange OR :ssc " + - "WHERE vmInstanceUuid = :vmUuid") - .param("vmUuid", vmInstanceUuid) - .param("ssc", storageStructureChange) - .execute(); - - // Q19 修复:INSERT IGNORE 返回 0(行已存在)但 UPDATE 也返回 0(行被并发删除) - // 竞态窗口:INSERT IGNORE → onFlushSuccess DELETE → UPDATE(行已不存在) - // 此时必须重新 INSERT,否则本次 markDirty 对应的 DB 变更将丢失 - if (updated == 0) { - SQL.New("INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + - "VALUES (:vmUuid, 1, :ssc)") - .param("vmUuid", vmInstanceUuid) - .param("ssc", storageStructureChange) - .execute(); - } - } - - // 立即唤醒:尝试认领并提交刷写,不等待 Poller 轮询 - triggerFlushForVm(vmInstanceUuid); - return true; - } catch (Exception e) { - logger.warn("markDirty failed for vm={}: {}", vmInstanceUuid, e.getMessage()); - return false; - } -} -``` - -### 竞态分析与修复 - -**问题**:`INSERT IGNORE` 与 `UPDATE` 是两个非原子操作,存在以下竞态窗口: - -``` -T1: API-A 修改 DB -T2: API-A 调用 markDirty → INSERT IGNORE → inserted=0(行已存在,dirtyVersion=N) -T3: Poller flush 完成 → onFlushSuccess: DELETE WHERE dirtyVersion=N → 行被删除 -T4: API-A: UPDATE WHERE vmInstanceUuid=:vmUuid → updated=0(行已不存在) -T5: API-A 调用 triggerFlushForVm → 无 dirty 行 → skip -→ API-A 的 DB 变更未被 flush 刷写! -``` - -**关键**:T3 的 flush 读 DB 快照在 T1 之前(flush 早于 API-A 的 DB 变更),因此写入的元数据不包含 API-A 的修改。T3 DELETE 成功因为 `dirtyVersion` 未被递增(T4 还未执行)。 - -**修复**:当 `inserted == 0 && updated == 0` 时,重新执行 `INSERT IGNORE`。使用 `INSERT IGNORE` 而非 `INSERT` 保证并发安全(若另一线程同时插入,IGNORE 避免异常)。重新创建的行 `dirtyVersion=1`,Poller/triggerFlush 将从 DB 全量读取最新状态并刷写。 - -**修复后时序**: - -``` -T1: API-A 修改 DB -T2: INSERT IGNORE → inserted=0 -T3: onFlushSuccess DELETE → 行被删除 -T4: UPDATE → updated=0 -T5: 重新 INSERT IGNORE → 成功,dirtyVersion=1 -T6: triggerFlushForVm → 认领新行 → flush 读 DB(包含 API-A 的修改)→ 写入 (Y) -``` - -/** - * 便捷重载:默认 storageStructureChange=false(CONFIG 级别)。 - */ -public boolean markDirty(String vmInstanceUuid) { - return markDirty(vmInstanceUuid, false); -} -``` - -### 为什么 markDirty 需要检查 `vm.metadata.enabled`? - -需要检查。虽然 `VmMetadataUpdateInterceptor` 层已检查功能开关,但 markDirty 还有其他调用方(级联删除、HA 回调、巡检恢复等),这些调用方未必都做了检查。在 markDirty 内统一检查是防御性编程的最低成本方案。 - -### 为什么不重置 retryCount? - -如果 PS 持续不可用,连续 API 触发的 markDirty 不应重置重试计数器,否则永远不会触达上限告警。retryCount 仅在**刷写成功**时重置为 0。 - -### 为什么不修改 managementNodeUuid? - -若 Poller 已认领此行正在刷写,markDirty 不应抢走它。`dirtyVersion` 递增后,刷写完成时会通过版本号比较发现“有新变更”,自动释放让下轮重处理(见 §4.5)。 - -### markDirty 后立即唤醒 - -markDirty 后立即调用 `triggerFlushForVm(vmUuid)` 尝试认领并提交刷写,消除最长 5s 的 Poller 等待延迟。Poller 降级为**安全网**,负责处理:退避中的行、MN 宕机后释放的行、triggerFlush 未能认领的行。 - -```java -/** - * 立即尝试认领并刷写指定 VM 的 dirty 行。 - * 若行已被认领或处于退避期,跳过(Poller 安全网会处理)。 - */ -private void triggerFlushForVm(String vmUuid) { - String myId = Platform.getManagementServerId(); - // Q20 修复:findStaleClaimOwner 可能返回 null(无 stale claim)。 - // SQL 的 OR 分支使用 :staleId 参数,当 staleId=null 时 - // MySQL 会将 `managementNodeUuid = NULL` 解析为 FALSE(SQL 三值逻辑), - // 不会误匹配任何行。但为避免依赖此隐式行为,显式处理: - // staleId=null 时仅使用 IS NULL 分支,不包含 stale 接管条件。 - String staleId = findStaleClaimOwner(vmUuid, Duration.ofMinutes(10)); - - String sql; - if (staleId != null) { - sql = "UPDATE VmMetadataDirtyVO " + - "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + - "WHERE vmInstanceUuid = :vmUuid " + - "AND (managementNodeUuid IS NULL " + - " OR (managementNodeUuid = :staleId AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE)) " + // 10 → vm.metadata.triggerFlush.staleMinutes - "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; - } else { - sql = "UPDATE VmMetadataDirtyVO " + - "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + - "WHERE vmInstanceUuid = :vmUuid " + - "AND managementNodeUuid IS NULL " + - "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP)"; - } - - int claimed = SQL.New(sql) - .param("myId", myId) - .param("staleId", staleId) // null-safe: only used when staleId != null - .param("vmUuid", vmUuid) - .execute(); - - if (claimed == 0) { - logger.debug("triggerFlushForVm skip claim, vmUuid={}, reason=already-claimed-or-backoff", vmUuid); // Q2-3 - return; - } - - VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); - // DP-07 说明:dirty == null 是合法场景。CAS UPDATE 成功(claimed > 0)后、findByUuid 前, - // 若同 MN 上一个 running flush 的 onFlushSuccess() 恰好执行了条件 DELETE - // (dirtyVersion 未被新 markDirty 递增),则该行已被删除。 - // 此时直接 return 即可,无需告警——数据已经是最新的。 - if (dirty == null) return; - - submitFlushTask(dirty); // 提交到 ChainTask(同 Poller 路径) -} -``` - -**退避中的行不会被立即唤醒**:若 dirty 行处于指数退避(`nextRetryTime > NOW()`),triggerFlush 的 WHERE 条件将其排除。这是有意设计——退避意味着 PS 可能不可用,markDirty 带来的新变更会在退避到期后由 Poller 一并处理。 - -**DP-06 分析:长时间 flush 与 stale claim 接管的交互**(补充说明) - -`triggerFlushForVm()` 的 stale claim 接管阈值为 **10 分钟**,而 `claimDirtyRows()` 的僵尸清理阈值为 **15 分钟**。两者的不对称设计有以下意图: - -| 路径 | 阈值 | 理由 | -|------|------|------| -| `triggerFlushForVm` stale 接管 | 10 min | API 热路径,优先保证响应性 | -| `cleanupZombieClaims` 僵尸清理 | 15 min | 批量路径,保守策略避免误抢 | - -**潜在问题**:若某次 `doFlush` 因 PS 慢响应耗时 8-12 分钟(未超 5 分钟消息超时但含排队等待), -`triggerFlushForVm()` 可能在第 10 分钟接管该行的 claim,而原 flush 任务仍在 ChainTask 中运行, -导致同一 VM 短暂出现两个并发 flush 意图。由于 per-VM ChainTask `syncLevel=1`, -实际执行仍是串行的,不会产生数据不一致。但建议后续考虑引入 `flushStartTime` 字段, -让 stale 判断基于「flush 实际开始时间」而非「claim 时间」,避免误判。 - -## 3.2 调用位置 - -| 调用方 | 场景 | 说明 | -|--------|------|------| -| `VmMetadataUpdateInterceptor.beforePublishEvent()` | `@MetadataImpact` API 成功后 | 主流程 | -| `MetadataCascadeExtension.asyncCascade()` | 级联删除 Volume/Snapshot | 非 API 内部操作 | -| HA handler 完成回调 | HA 重启 VM | 非 API 内部操作 | -| 定时快照清理 handler | 快照删除 | 非 API 内部操作 | -| 内部卷迁移 handler | installPath 变更 | 非 API 内部操作 | -| 升级全量刷新 | 版本变更后批量触发 | 见 §9 | - -**两道防线**: - -1. **开发规范**:修改 VM 存储拓扑字段的内部消息处理器,成功后必须调用 `markDirty()` -2. **路径指纹巡检兜底**:每次刷写成功后记录 VM 的全量路径快照,独立 PeriodicTask 周期性比对 DB 当前路径 vs 快照,不一致则 `markDirty()`(见 §8.2) - -对注册场景,即使元数据暂时落后于 DB,Part 3 §3.4 的 installPath 存在性检查提供额外兗底。 - -与 GC 方案 submit 的详细对比见 [对比文档 §2](2/vm-metadata-new-02h-compare.md#2-标脏入口对比markdirty-vs-gc-submit)。 - ---- - -# 4. MetadataDirtyPoller — 轮询刷写 - -## 4.1 基本结构 - -```java -public class MetadataDirtyPoller implements PeriodicTask { - @Override - public TimeUnit getTimeUnit() { return TimeUnit.SECONDS; } - - @Override - public long getInterval() { - return VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.value(Long.class); - // 默认 5 秒,可通过 GlobalConfig 动态调整 - } - - @Override - public String getName() { return "vm-metadata-dirty-poller"; } - - @Override - public void run() { - claimAndFlush(); - } -} -``` - -启动:在 `managementNodeReady()` 中 `thdf.submitPeriodicTask(new MetadataDirtyPoller())`。 - -GlobalConfig 变更时自动重启 Poller(与 SecurityGroup FailureHostWorker 一致): - -```java -VmGlobalConfig.VM_METADATA_DIRTY_POLL_INTERVAL.installUpdateExtension((oldValue, newValue) -> { - restartPoller(); -}); -``` - -**Poller 角色定位**:markDirty 后立即调用 `triggerFlushForVm()` 已覆盖常规场景(见 §3.1)。Poller 降级为**安全网**,负责处理: -- 退避中的行(`nextRetryTime` 到期后才能认领) -- MN 宕机后 FK SET_NULL 释放的孤儿行 -- triggerFlushForVm 认领失败的行(已被其他 MN Poller 认领) - -## 4.2 认领(CAS 方式) - -采用 CAS(单条 UPDATE WHERE NULL LIMIT N),比悲观锁更简洁,避免死锁风险。 - -```java -private List claimDirtyRows() { - // DP-05 修复:僵尸 claim 清理从 claimDirtyRows() 提取为独立低频任务。 - // 原实现在每次 Poller 周期(5s)执行带 write-intent 的 UPDATE 扫描, - // 增加了不必要的数据库压力。改为 cleanupZombieClaims() 独立定时执行(见下方)。 - - // Step 1: CAS 原子认领 — 单条 UPDATE 天然原子 - String sql = "UPDATE VmMetadataDirtyVO " + - "SET managementNodeUuid = :myId, lastClaimTime = CURRENT_TIMESTAMP " + - "WHERE managementNodeUuid IS NULL " + - "AND (nextRetryTime IS NULL OR nextRetryTime <= CURRENT_TIMESTAMP) " + - "ORDER BY lastOpDate ASC, vmInstanceUuid ASC " + // Q17 修复:按最后标脏时间排序(最早变更优先),vmInstanceUuid 作为稳定 tiebreaker - // 原 ORDER BY dirtyVersion ASC 有误:dirtyVersion 是 per-row 值(每行从 1 开始), - // 不同 VM 的 dirtyVersion 无序且可能相等,无法反映全局标脏顺序。 - // lastOpDate 由 MySQL ON UPDATE CURRENT_TIMESTAMP 自动维护,反映最近一次 markDirty 时间, - // 适合作为公平调度指标。秒级精度足够(Poller 周期 5s >> 1s 精度)。 - "LIMIT :batchSize"; - - int claimed = SQL.New(sql) - .param("myId", Platform.getManagementServerId()) - .param("batchSize", VmGlobalConfig.VM_METADATA_DIRTY_BATCH_SIZE.value(Integer.class)) - .execute(); - - if (claimed == 0) return Collections.emptyList(); - - // Step 2: 查询刚认领到的行(DP-01 修复:增加 lastClaimTime 过滤, - // 仅返回本轮 CAS 认领的行,避免与 triggerFlushForVm 并发认领的行混入) - // Q18 说明:thisCycleCutoff = now - 5s 是 Poller 周期的上界。若某轮 Poller 执行(含 CAS UPDATE) - // 耗时超过 5s(极端负载),cutoff 可能过滤掉本轮认领的行。但这仅导致那些行不被本轮处理, - // 下轮 Poller 仍会发现它们(已 claimed by this MN + lastClaimTime 匹配)。 - // 更精确的做法是在 Step 1 CAS 前记录 beforeClaim = CURRENT_TIMESTAMP, - // Step 2 使用 gte(lastClaimTime, beforeClaim)。但引入 Java↔DB 时间偏差风险。 - // 当前方案的 5s 余量足够覆盖 99.99% 场景,接受此权衡。 - Timestamp thisCycleCutoff = Timestamp.from(Instant.now().minus(Duration.ofSeconds(5))); - return Q.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.managementNodeUuid, Platform.getManagementServerId()) - .gte(VmMetadataDirtyVO_.lastClaimTime, thisCycleCutoff) - .list(); -} -``` - -**DP-05 改进:僵尸 claim 清理独立为低频任务** - -```java -/** - * 独立的僵尸 claim 清理任务(防御性措施)。 - * 从 claimDirtyRows() 提取,以避免每 5s Poller 周期执行不必要的 write-intent 扫描。 - * 建议间隔:60s(1 分钟),远低于 15 分钟僵尸阈值,足以及时发现异常。 - * - * 僵尸清理的必要性分析: - * MN 正常崩溃 → FK SET NULL 立即释放 claim,Poller 下轮即可重认领。 - * 本任务覆盖的是 FK SET NULL 无法触发的场景: - * (a) MN 进程 hang 住(JVM 死锁 / 长 GC),心跳未失效但 flush 永久阻塞; - * (b) 网络分区导致目标 Agent 无响应,ChainTask 在 timeout 前持续持有 claim; - * (c) 极端:MN 已离线但 ManagementNodeVO 记录因 heartbeat 延迟尚未被清理。 - * 15 分钟阈值 > flush 最大超时(5×60s=5min),安全余量充足。 - */ -@PeriodicTask(interval = 60, unit = TimeUnit.SECONDS) -private void cleanupZombieClaims() { - SQL.New("UPDATE VmMetadataDirtyVO " + - "SET managementNodeUuid = NULL, lastClaimTime = NULL " + - "WHERE managementNodeUuid IS NOT NULL " + - "AND lastClaimTime < CURRENT_TIMESTAMP - INTERVAL 15 MINUTE") - .execute(); -} -``` - -**说明**:`triggerFlushForVm()` 单 VM 抢占路径允许"stale claim 接管"(10 分钟),Poller 批量路径采用"先清理僵尸再 CAS"的保守策略(15 分钟),避免误抢活跃任务。 - -**CAS vs 悲观锁**: - -| | CAS (UPDATE WHERE NULL) | 悲观锁 (SELECT FOR UPDATE) | -|---|---|---| -| 原子性 | 单条 UPDATE 天然原子 | 需事务包裹 SELECT + UPDATE | -| 死锁风险 | 无 | 双 MN 可能死锁 | -| 性能 | 无锁等待 | 有锁等待 | -| 实现复杂度 | 低 | 中 | - -**MySQL 行锁分析**:CAS 的 `UPDATE ... WHERE managementNodeUuid IS NULL AND ... LIMIT N` 在 InnoDB 中会对满足 WHERE 条件的行加 **X 锁**(排他锁)。双 MN 并发执行时,先执行的 UPDATE 获得行锁并将 `managementNodeUuid` 置为非 NULL,后执行的 UPDATE 的 WHERE 条件不再匹配该行,`affected_rows=0`。`LIMIT N` 保证每次最多锁定 N 行,并发窗口极短(微秒级),不会引发死锁。 - -## 4.3 刷写(Flush) - -认领成功后,对每个 dirty row 提交到 ChainTask 执行刷写: - -```java -private void claimAndFlush() { - List claimed = claimDirtyRows(); - for (VmMetadataDirtyVO dirty : claimed) { - submitFlushTask(dirty); - } -} - -private void submitFlushTask(VmMetadataDirtyVO dirty) { - // 讨论 Δ-1:原方案为嵌套 ChainTask(外层全局限流 + 内层 per-VM 串行)。 - // 重构为单层 per-VM ChainTask + AtomicInteger 全局限流,原因: - // 1. 嵌套 ChainTask 的 outerChain.next() 必须在 innerChain 完成后调用, - // 但 exceedMaxPendingCallback 中 outerChain.next() 直接调用导致 - // outer slot 提前释放,全局限流语义被破坏。 - // 2. 嵌套结构难以推断 Chain 生命周期,增加维护和调试成本。 - // 3. AtomicInteger 全局计数器语义简单明确:flush 开始 increment、 - // 完成(成功/失败/exceed)decrement,超限时 skip。 - // - // 新结构: - // - 全局 AtomicInteger globalFlushInFlight(初始 0) - // - submitFlushTask 先检查 globalFlushInFlight < maxConcurrent, - // 超限时释放 claim 并 return - // - 通过则 increment,提交到 per-VM ChainTask(syncLevel=1, maxPending=1) - // - doFlush 完成回调中 decrement - - int maxConcurrent = VmGlobalConfig.VM_METADATA_GLOBAL_MAX_CONCURRENT.value(Integer.class); - if (globalFlushInFlight.get() >= maxConcurrent) { - // 全局并发已满,释放 claim,Poller 下轮重试 - releaseClaim(dirty.getVmInstanceUuid()); - return; - } - globalFlushInFlight.incrementAndGet(); - - // 单层 per-VM 串行 + 去重 - thdf.chainSubmit(new ChainTask(null) { - @Override - public String getSyncSignature() { - return "update-vm-" + dirty.getVmInstanceUuid() + "-metadata"; - } - @Override - public int getSyncLevel() { return 1; } - @Override - public int getMaxPendingTasks() { return 1; } - @Override - public String getDeduplicateString() { return getSyncSignature(); } - - @Override - public void exceedMaxPendingCallback() { - // 已有 running + pending,本次多余 - // Δ-1 改进:在单层结构中,exceed 时直接 decrement 并释放 claim - globalFlushInFlight.decrementAndGet(); - releaseClaim(dirty.getVmInstanceUuid()); - } - - @Override - public void run(SyncTaskChain chain) { - doFlush(dirty, () -> { - globalFlushInFlight.decrementAndGet(); - chain.next(); - }); - } - }); -} -``` - -## 4.4 doFlush 核心逻辑 - -```java -private void doFlush(VmMetadataDirtyVO dirty, Runnable chainNext) { - String vmUuid = dirty.getVmInstanceUuid(); - - // P2 修复:重新从 DB 读取 dirty 行,获取最新的 storageStructureChange 和 dirtyVersion。 - // 原因:submitFlushTask 传入的 dirty 对象是 CAS 认领时的缓存快照,排队等待期间 - // 可能有新的 markDirty(storageStructureChange=true) 通过 OR 升级了该字段。 - // 若使用缓存值,会导致本轮 flush 的 storageStructureChange=false, - // 而 DB 中实际已为 true(如存储迁移触发的 markDirty),写入时用错 tmp 后缀。 - VmMetadataDirtyVO latestDirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); - if (latestDirty == null) { - // VM 已删除(FK CASCADE)或 onFlushSuccess 已删除该行 - chainNext.run(); - return; - } - - // 0. 记录刷写开始时的 dirtyVersion 快照(使用最新值) - long snapshotVersion = latestDirty.getDirtyVersion(); - - // 1. 前置检查:VM 是否存在 - if (!dbf.isExist(vmUuid, VmInstanceVO.class)) { - // VM 已删除,FK CASCADE 应已删除 dirty 行,兜底删除 - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); - chainNext.run(); - return; - } - - // 1b. Q34 修复:过滤 Destroyed 状态的 VM - // VM 正在销毁过程中(state=Destroyed),EO 尚未物理删除,FK CASCADE 未触发。 - // 此时刷写元数据无意义——销毁完成后 EO 删除时 dirty 行会被级联清理。 - // 主动删除 dirty 行释放 Poller 资源,避免对即将销毁的 VM 执行无效的 Agent 调用。 - VmInstanceState vmState = Q.New(VmInstanceVO.class) - .eq(VmInstanceVO_.uuid, vmUuid) - .select(VmInstanceVO_.state) - .findValue(); - if (vmState == VmInstanceState.Destroyed) { - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); - chainNext.run(); - return; - } - - // 2. 发送 UpdateVmInstanceMetadataMsg → VmInstanceBase 负责构建 payload 并写入主存储 - // payload 构建(buildVmInstanceMetadata)和大小保护均在 VmInstanceBase 内部完成 - UpdateVmInstanceMetadataMsg msg = new UpdateVmInstanceMetadataMsg(); - msg.setUuid(vmUuid); - msg.setStorageStructureChange(latestDirty.isStorageStructureChange()); - msg.setTimeout(TimeUnit.MINUTES.toMillis(5)); - bus.makeLocalServiceId(msg, VmInstanceConstant.SERVICE_ID); - - bus.send(msg, new CloudBusCallBack(null) { - @Override - public void run(MessageReply reply) { - if (reply.isSuccess()) { - onFlushSuccess(vmUuid, snapshotVersion); - } else { - onFlushFailure(vmUuid, reply.getError()); - } - chainNext.run(); - } - }); -} -``` - -## 4.5 刷写成功处理 - -```java -// DP-04 修复 + 讨论 Δ-2:原方案使用 @Transactional 包装 DELETE + fallback UPDATE。 -// 改为 SQLBatch 替代 @Transactional,原因: -// 1. @Transactional 由 Spring AOP 代理实现,要求方法为 public 且通过代理对象调用。 -// onFlushSuccess 作为内部回调方法,直接调用(this.onFlushSuccess)不经过代理, -// @Transactional 不生效("self-invocation 陷阱")。 -// 2. SQLBatch 是 ZStack 原生事务工具,无代理依赖,显式包装事务边界, -// 在 callback/lambda 场景中更可靠。 -// 3. 逻辑不变:DELETE + fallback UPDATE 仍在同一事务内原子执行。 -private void onFlushSuccess(String vmUuid, long snapshotVersion) { - new SQLBatch() { - @Override - protected void scripts() { - // 条件删除:仅当 dirtyVersion == snapshotVersion 时删除 - // 即"刷写期间没有新的 markDirty 到来" - int deleted = SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) - .eq(VmMetadataDirtyVO_.dirtyVersion, snapshotVersion) - .delete(); - - if (deleted == 0) { - // dirtyVersion > snapshotVersion → 刷写期间有新变更 - // 释放认领,让 triggerFlush / Poller 重新处理 - // 同时重置 retryCount(本次成功说明通路正常) - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) - .set(VmMetadataDirtyVO_.managementNodeUuid, null) - .set(VmMetadataDirtyVO_.retryCount, 0) - .set(VmMetadataDirtyVO_.nextRetryTime, null) - .update(); - } - // deleted > 0 → 行已删除,彻底完成 - } - }.execute(); - - // 讨论 Δ-9:savePathFingerprint 复用 buildVmInstanceMetadata 的 installPath 列表。 - // 原方案在 savePathFingerprint 中独立查询 VolumeVO + VolumeSnapshotVO。 - // 改为 buildVmInstanceMetadata 返回复合对象(含 payload + installPath list), - // onFlushSuccess 直接传入 installPath list 给 savePathFingerprint, - // 避免重复查询,减少一次 DB roundtrip。 - // 具体实现:doFlush 中 buildVmInstanceMetadata 返回 BuildResult{payload, pathSnapshot}, - // onFlushSuccess(vmUuid, snapshotVersion, pathSnapshot) 传入预计算的 pathSnapshot。 - savePathFingerprint(vmUuid); -} -``` - -**这是整个方案最关键的设计点**。`dirtyVersion` 比较确保不会丢失刷写期间产生的新变更: - -``` -T0: markDirty(vm-1) → INSERT, dirtyVersion=1 -T1: 认领 → snapshotVersion=1 -T2: 刷写进行中... buildVmInstanceMetadata() 读到 v1 -T3: API 成功 → markDirty(vm-1) → dirtyVersion=2 ← 新变更! -T4: 刷写完成,写入 v1 -T5: onFlushSuccess → DELETE WHERE dirtyVersion = 1 - → 当前 dirtyVersion=2 ≠ 1 → deleted=0 - → 释放认领 → triggerFlush 立即重处理 → 读到 v2 → 写入 v2 (Y) -``` - -如果不做 `dirtyVersion` 比较直接删除,T3 的变更就丢了——这正是 GC `deduplicateSubmit` 遇到的同类问题,新方案用版本号比较优雅解决。相比 `lastOpDate` 时间戳比较,`dirtyVersion` 整数比较语义更明确、无时间精度问题。 - -## 4.6 刷写失败处理 - -```java -private void onFlushFailure(String vmUuid, ErrorCode error) { - // Q21 — 原子性分析:先 findByUuid 再 UPDATE 存在微窗口(读到的 retryCount 可能被 - // 并发 markDirty 改变)。但 markDirty 不修改 retryCount(仅递增 dirtyVersion), - // 且同一 VM 同一时刻只有一个 flush 任务(Layer 1 CAS + Layer 3 per-VM syncLevel=1), - // 因此 onFlushFailure 的 findByUuid→UPDATE 在同 VM 上无并发竞争。安全。 - VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); - if (dirty == null) return; // VM 已销毁,FK CASCADE 已清理 - - int newRetryCount = dirty.getRetryCount() + 1; - int maxRetry = VmGlobalConfig.VM_METADATA_MAX_RETRY.value(Integer.class); // 默认 5 - int baseDelay = VmGlobalConfig.VM_METADATA_RETRY_BASE_DELAY_SECONDS.value(Integer.class); // Q2-6 - int maxExponent = VmGlobalConfig.VM_METADATA_RETRY_MAX_EXPONENT.value(Integer.class); // Q2-6 - - if (newRetryCount >= maxRetry) { - // 达到上限 → 告警 + 标记 stale(H2 修复:不再直接删除) - logger.error("metadata update for vm {} failed after {} retries, marking as stale. " + - "MetadataStaleRecoveryTask will retry independently.", - vmUuid, newRetryCount); - - // 在 PathFingerprintVO 上标记 lastFlushFailed=true(M1 修复) - SQL.New("UPDATE VmMetadataPathFingerprintVO " + - "SET lastFlushFailed = 1 WHERE vmInstanceUuid = :vmUuid") - .param("vmUuid", vmUuid) - .execute(); - - // 删除 dirty 行(释放 Poller 资源),stale 恢复由独立任务接管 - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid).delete(); - return; - } - - // 未达上限 → 释放认领 + 指数退避(Q2-6: 参数改为 GlobalConfig) - long delaySec = baseDelay * (1L << Math.min(newRetryCount, maxExponent)); - Timestamp nextRetry = Timestamp.from(Instant.now().plusSeconds(delaySec)); - - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) - .set(VmMetadataDirtyVO_.managementNodeUuid, null) // 释放认领 - .set(VmMetadataDirtyVO_.retryCount, newRetryCount) - .set(VmMetadataDirtyVO_.nextRetryTime, nextRetry) - .update(); - - logger.warn("metadata update for vm {} failed (retry {}/{}), next retry at {}", - vmUuid, newRetryCount, maxRetry, nextRetry); -} -``` - -**指数退避表**: - -| 尝试次数 | retryCount 变化 | 下次退避延迟 | 累计耗时 | -|----------|-----------------|-------------|----------| -| 1 | 0 → 1 | 20s | ~25s | -| 2 | 1 → 2 | 40s | ~65s | -| 3 | 2 → 3 | 80s | ~145s | -| 4 | 3 → 4 | 160s | ~305s | -| 5 | 4 → 5 | — | 放弃 | - -延迟公式:`vm.metadata.retry.baseDelaySeconds × 2^min(retryCount, vm.metadata.retry.maxExponent)`;默认值分别为 `10`、`10`。默认 5 次重试,总耗时约 5 分钟后放弃。 - -## 4.7 辅助方法 - -```java -private void releaseClaim(String vmUuid) { - SQL.New(VmMetadataDirtyVO.class) - .eq(VmMetadataDirtyVO_.vmInstanceUuid, vmUuid) - .set(VmMetadataDirtyVO_.managementNodeUuid, null) - .update(); -} -``` - -## 4.8 Stale 恢复任务(H2 修复) - -当 dirty 行因重试耗尽被删除后,低频 VM(长期无 `@MetadataImpact` API)将失去自愈机会。为此引入独立的 `MetadataStaleRecoveryTask`: - -```java -public class MetadataStaleRecoveryTask implements PeriodicTask { - @Override - public long getInterval() { - return VmGlobalConfig.VM_METADATA_STALE_RECOVERY_INTERVAL.value(Long.class); - // 默认 1800 秒(30 分钟) - } - - @Override - public void run() { - // 查找所有 lastFlushFailed=true 的指纹记录 - List staleVms = SQL.New( - "SELECT fp FROM VmMetadataPathFingerprintVO fp WHERE fp.lastFlushFailed = 1", - VmMetadataPathFingerprintVO.class) - .limit(VmGlobalConfig.VM_METADATA_STALE_RECOVERY_BATCH.value(Integer.class)) // 默认 100 - .list(); - - for (VmMetadataPathFingerprintVO fp : staleVms) { - // 重新 markDirty,给予全新的重试机会(retryCount=0) - // DP-03 修复:先验证 markDirty 成功,再清除 stale 标记; - // 若 markDirty 失败(如 DB 连接异常),保留 lastFlushFailed=true, - // 下一轮 StaleRecoveryTask 会重试。 - boolean markSuccess = markDirty(fp.getVmInstanceUuid()); - if (markSuccess) { - // markDirty 成功 → 安全清除 stale 标记(由下轮 Poller 处理) - SQL.New("UPDATE VmMetadataPathFingerprintVO " + - "SET lastFlushFailed = 0 WHERE vmInstanceUuid = :vmUuid") - .param("vmUuid", fp.getVmInstanceUuid()) - .execute(); - } else { - // markDirty 失败 → 保留 lastFlushFailed=true,记录日志 - logger.warn("MetadataStaleRecoveryTask: markDirty failed for vm={}, " + - "keeping lastFlushFailed=true for next retry cycle", - fp.getVmInstanceUuid()); - } - } - - if (!staleVms.isEmpty()) { - logger.info("MetadataStaleRecoveryTask re-queued {} stale VMs for retry", staleVms.size()); - } - } -} -``` - -**关键设计点**: -- 独立于 Poller 的 PeriodicTask,不受 Poller 退避机制约束 -- 每 30 分钟扫描一次,每次最多处理 100 个 stale VM -- 重新 `markDirty()` 给予全新重试机会(retryCount=0),不继承历史退避 -- **DP-03 修复**:`markDirty()` 返回 boolean,仅在成功时才清除 `lastFlushFailed`;失败时保留标记,下轮再试 -- 若 PS 仍不可用,该 VM 会再次走完 Poller 的 5 次重试 → 再次标记 stale → 30 分钟后再次恢复,形成"慢速重试"闭环 -- 当 PS 恢复可用时,下一轮 stale recovery 触发的 markDirty 自然成功 - ---- - -# 5. 消息调用链 - -## 5.1 新调用链 - -``` -API (e.g. StartVmInstanceMsg) 成功 - ↓ -VmMetadataUpdateInterceptor.beforePublishEvent() - ↓ -markDirty(vmUuid) ← INSERT/UPDATE + dirtyVersion++,本地操作,无跨 MN - ↓ -triggerFlushForVm(vmUuid) ← 立即唤醒:CAS 认领单行 + 提交 ChainTask - ↓(认领失败时由 Poller 安全网兆底,≤5s) - ↓ AtomicInteger globalFlushInFlight 检查(Δ-1:替代原嵌套 ChainTask 外层) - ↓ per-VM ChainTask "update-vm-{vmUuid}-metadata" (syncLevel=1, maxPending=1) - ↓ -doFlush() - → bus.send(UpdateVmInstanceMetadataMsg) → makeLocalServiceId - ↓ -VmInstanceBase.handle(UpdateVmInstanceMetadataMsg) - → buildVmInstanceMetadata(vmUuid) — DB 全量读取(@Transactional(readOnly=true)) - → payload 大小保护(>8MB 告警, >30MB 拒绝) - ↓ -bus.send(UpdateVmInstanceMetadataOnPrimaryStorageMsg) → makeLocalServiceId - ↓ -NFS/LocalStorage/SharedBlock.handle() - ↓ ChainTask "update-metadata-on-ps-{psUuid}" - ↓ 选取 Host → UpdateVmInstanceMetadataOnHypervisorMsg - ↓ makeTargetServiceIdByResourceUuid(hostUuid) ← 保留 hash 环路由 - ↓ -HostBase.handle() → HTTP call to KVM agent - ↓ -成功 → onFlushSuccess() → 条件 DELETE -失败 → onFlushFailure() → 指数退避释放 -``` - -**OP type 由管理层面指定**:`@MetadataImpact(CONFIG)` → OP type=1(仅配置变更),`@MetadataImpact(STORAGE)` → OP type=2(存储拓扑变更,sblk 场景设置 pending_op=2)。OP type 通过 `storageStructureChange` 字段贯穿整条消息链(`VmMetadataDirtyVO` → `UpdateVmInstanceMetadataMsg` → `UpdateVmInstanceMetadataOnPrimaryStorageMsg` → `UpdateVmInstanceMetadataOnHypervisorMsg`)。dirty 行使用 OR 升级策略:多次 markDirty 中只要有一次是 STORAGE,本轮刷写即使用 OP type=2。 - -**消息超时**:`UpdateVmInstanceMetadataMsg` 设置为 `5min`(防止内层任务 hang 导致 claim 长期占用);`UpdateVmInstanceMetadataOnHypervisorMsg` 保持 `2min`。超时后统一进入 `onFlushFailure()` 释放认领并退避重试。 - -与 GC 方案消息链的详细对比见 [对比文档 §3](2/vm-metadata-new-02h-compare.md#3-消息调用链对比)。 - -## 5.2 消息路由策略 - -| 消息 | 路由方式 | 说明 | -|------|----------|------| -| `UpdateVmInstanceMetadataMsg` | `makeLocalServiceId` | Poller 本地发起 | -| `UpdateVmInstanceMetadataOnPrimaryStorageMsg` | `makeLocalServiceId` | 无本地状态依赖 | -| `UpdateVmInstanceMetadataOnHypervisorMsg` | `makeTargetServiceIdByResourceUuid(hostUuid)` | 需路由到 host-owner MN | - ---- - -# 6. 并发控制(四层) - -## 6.1 四层串行化保证 - -``` -Layer 1 — DB CAS 认领 - UPDATE WHERE managementNodeUuid IS NULL → 同一行只被一个 MN 处理 - ⇒ 同一 VM 的刷写不会在两个 MN 上同时执行 - -Layer 2 — 全局限流(AtomicInteger) - globalFlushInFlight AtomicInteger (默认上限 10,可通过 GlobalConfig 调整) - ⇒ 同一 MN 最多 N 个 VM 同时更新 - 讨论 Δ-1 变更:原方案为嵌套 ChainTask 外层全局队列, - 改为 AtomicInteger 计数器。语义等价但消除了嵌套 Chain 的复杂性。 - submitFlushTask 入口检查 get() >= maxConcurrent 时直接 releaseClaim 跳过。 - -Layer 3 — per-VM 串行队列 "update-vm-{vmUuid}-metadata" - syncLevel=1, maxPendingTasks=1 - ⇒ 同一 VM 最多 1 个正在执行 + 1 个排队 - ⇒ 超出时 exceedMaxPendingCallback() → decrementAndGet + releaseClaim - -Layer 4 — 主存储级队列 "update-metadata-on-ps-{psUuid}" - syncLevel = vm.metadata.ps.maxConcurrent (GlobalConfig, 默认 5) - ⇒ 同一 MN 上,同一存储最多 N 个并发写入 - ⇒ 双 MN 环境下实际全局并发 = 2 × syncLevel -``` - -与 GC 方案并发控制的详细对比见 [对比文档 §4](2/vm-metadata-new-02h-compare.md#4-并发控制对比)。 - -## 6.2 全局限流 - -**讨论 Δ-1 重构后结构**: - -原嵌套 ChainTask 结构已简化为单层结构: - -``` -AtomicInteger globalFlushInFlight (上限 = vm.metadata.global.maxConcurrent, 默认 10) - └── per-VM ChainTask: syncSignature = "update-vm-{vmUuid}-metadata" - syncLevel = 1, maxPendingTasks = 1, deduplicateString = syncSignature -``` - -- `globalFlushInFlight` 控制全局并发数,每个 MN 最多 N 个 VM 同时更新 -- per-VM ChainTask 保证 per-VM 串行 + 去重 -- `exceedMaxPendingCallback` 中直接 `decrementAndGet()` + `releaseClaim()`,不再持有 claim - -**per-MN 语义**:`globalFlushInFlight` 是 JVM 本地计数器。双 MN 环境下实际全局并发最大为 `2 × maxConcurrent`。DB CAS 认领已保证同一 VM 不会在两个 MN 上同时执行。 - -## 6.3 Layer 3 实现位置 - -各主存储 `handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg)` 内部用 `thdf.chainSubmit()` 包装: - -- `getSyncSignature()` → `"update-metadata-on-ps-" + self.getUuid()` -- `getSyncLevel()` → 读取 `VmGlobalConfig.VM_METADATA_PS_MAX_CONCURRENT` -- `run()` → 调用实际写入逻辑后 `chain.next()` - -**外层全局计数器与 Layer 4 的交互**:AtomicInteger `globalFlushInFlight` 上限为 10(默认),限制单个 MN 上同时最多 10 个 VM 的元数据更新在执行。这 10 个并发任务分布在不同主存储上时,Layer 4 per-PS 队列 `syncLevel=5` 进一步约束同一存储的并发数。AtomicInteger 控制"总水位",Layer 4 控制"每个 PS 的分水位",二者共同生效。 - -**文档化要求**:`syncLevel` 和 AtomicInteger 全部为 JVM 本地语义。跨 MN 并发由 DB CAS 认领控制,不通过 ChainTask 全局共享队列。 - -## 6.4 调优指南 - -### 默认值推导 - -- `batchSize=50`:按平均每 VM flush 200ms 估算,50 台约 10s/轮;实际耗时受 `global.maxConcurrent=10` 并行限制。 -- `global.maxConcurrent=10`:管理节点线程池默认 500 线程,10 个并发约占 2%,对其他业务影响可控。 -- `ps.maxConcurrent=5`:限制单主存储写入并发,避免元数据 flush 风暴挤占业务 IO。 - -### 调优参考表 - -| 环境规模 | VM 数量 | batchSize | global.maxConcurrent | ps.maxConcurrent | pollInterval | -|----------|---------|-----------|----------------------|------------------|--------------| -| 小型 | <500 | 50 | 10 | 5 | 5s | -| 中型 | 500-5000 | 100 | 20 | 10 | 5s | -| 大型 | >5000 | 200 | 30 | 15 | 10s | - -### 调优顺序建议 - -1. 先调 `global.maxConcurrent`(观察 MN CPU/线程池饱和度); -2. 再调 `ps.maxConcurrent`(观察单 PS 延迟与业务 IO 干扰); -3. 最后调 `batchSize` 与 `pollInterval`(平衡吞吐与扫描开销)。 - ---- - -**文档拆分**:§7-§13(高可用、恢复策略、升级刷新、Payload 保护、开发约束、GlobalConfig)已迁移至 [Part 2b — 高可用与运维](vm-metadata-02b-高可用与运维.md)。 - -# 7. 约束与不変量 - -| 约束 ID | 约束描述 | 违反后果 | -|---------|----------|----------| -| C-DM-01 | `markDirty` 在集群模式下必须使用 `INSERT IGNORE + UPDATE` 两步,禁止回退为 `INSERT ON DUPLICATE KEY`。当 `inserted==0 && updated==0` 时必须重新 `INSERT IGNORE` 防止竞态丢失。**例外**:升级全量刷新场景(Part 2b §9)中批量 `markDirty` 可使用等效的批量 INSERT IGNORE + 批量 UPDATE 优化,但必须保持「先 INSERT IGNORE 再 UPDATE」的两步语义 | Galera 高并发下死锁概率上升,标脏链路抖动;竞态下 DB 变更丢失不被刷写 | -| C-CL-02 | 任何 claim 成功路径必须写入 `lastClaimTime`,并执行僵尸 claim 清理(15 分钟)。注:僵尸清理已独立为低频任务 `cleanupZombieClaims()`(DP-05) | hang 任务可能导致 dirty 行永久锁定 | -| C-TM-03 | `doFlush` 消息超时不得低于 5 分钟,且超时必须进入 `onFlushFailure` 释放 claim | inner task 卡死时无法自愈 | -| C-RB-04 | 指数退避参数必须来自 GlobalConfig(baseDelay/maxExponent),禁止硬编码常量 | 运维无法按环境调优重试节奏 | -| C-SR-05 | 重试耗尽时必须在 `VmMetadataPathFingerprintVO` 标记 `lastFlushFailed=true`,不得仅删除 dirty 行后静默放弃 | Stale VM 永久失去自愈路径 | -| C-SR-06 | `MetadataStaleRecoveryTask` 的 `markDirty()` 必须使用 retryCount=0(全新起点),不得继承历史退避。同时必须验证 `markDirty()` 返回值,仅在成功时清除 `lastFlushFailed`(DP-03) | 历史退避会导致立即再次耗尽;无条件清除可能永久丢失 stale 标记 | -| C-SC-07 | `storageStructureChange` 标记仅在真正影响存储拓扑的操作中设置(卷创建/删除/迁移/挂载/卸载),不得在纯属性修改(如改名、改描述)时误设。升级全量刷新场景中,`storageStructureChange` 应始终为 `true`(因为无法判断升级前后存储拓扑是否变化) | 误设 true → 触发不必要的 sblk 存储拓扑重建,增加 IO 开销;误设 false → 升级后存储拓扑变更未反映到 sblk | -| C-FL-08 | `doFlush` 必须在前置检查中过滤 `VmInstanceVO.state == Destroyed` 的 VM,主动删除 dirty 行释放 Poller 资源 | 对即将销毁的 VM 执行无效 Agent 调用,浪费资源并可能因 VM 关联存储正在清理而失败 | diff --git "a/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" "b/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" deleted file mode 100644 index 62cb071fb81..00000000000 --- "a/docs/design/vm-metadata-02b-\351\253\230\345\217\257\347\224\250\344\270\216\350\277\220\347\273\264.md" +++ /dev/null @@ -1,942 +0,0 @@ -# VM 元数据 — 高可用与运维 - -## 目录 - -7. [双 MN 高可用](#7-双-mn-高可用) -8. [管理平面恢复策略](#8-管理平面恢复策略) -9. [升级后全量刷新](#9-升级后全量刷新) -9a. [功能开关切换处理](#9a-功能开关切换处理) -10. [Payload 大小保护](#10-payload-大小保护) -11. [潜在代价与 tradeoff](#11-潜在代价与-tradeoff) -12. [开发约束清单](#12-开发约束清单) -13. [GlobalConfig 配置项汇总](#13-globalconfig-配置项汇总) -14. [可观测性指标](#14-可观测性指标) -15. [约束与不変量](#15-约束与不変量) - -**注意**:章节编号保持与原 Part 2 一致(§7-§13),以保证跨文档引用不变。§1-§6(数据模型、markDirty、Poller、消息链、并发控制)见 [Part 2 — Dirty Mark + Poller](vm-metadata-02-脏标记与Poller.md)。 - ---- - -# 7. 双 MN 高可用 - -## 7.1 为什么不需要 hash 环路由 - -`VmMetadataDirtyVO` 是 **共享 DB 表**,两个 MN 的 Poller 都能看到。认领通过 **DB CAS** 保证互斥,不依赖 JVM 本地状态——谁先认领谁处理,无需协调"谁是 owner"。 - -## 7.2 MN 宕机场景(自动恢复) - -``` -T0: MN-A Poller 认领 dirty(vm-1) - DB: {vmUuid:vm-1, managementNodeUuid:MN-A} - -T1: MN-A 宕机 - -T2: MN-B 心跳检测 → 删除 ManagementNodeVO(MN-A) - FK ON DELETE SET NULL → dirty(vm-1).managementNodeUuid = NULL - ← DB 约束自动完成,无需任何代码! - -T3: MN-B nodeLeft(MN-A) → 延迟 5s 后触发一轮 Poller - → 发现 vm-1 未认领 → CAS 认领 → 刷写 (Y) -``` - -**接管延迟**:心跳超时(~30s) + nodeLeft 延迟 5s 触发 ≈ **~35 秒** - -**M2 修复 — 延迟可配**:`nodeLeft` 延迟已通过 `vm.metadata.nodeLeft.delaySec`(§13)配置化(默认 5s)。对于网络抖动频繁的环境,运维可适当增大此值(如 10s)以扩大 in-flight flush 收敛窗口;对于需要快速接管的场景可减小至 3s。调整需与 Fence Check(§7.6)配合评估。 - -增加 `nodeLeft` 回调加速,但引入固定 5s 延迟避免与 dying MN 的 in-flight flush 窗口重叠。 - -```java -@Override -public void nodeLeft(ManagementNodeInventory inv) { - // MN 宕机 → FK SET_NULL 已释放其认领的 dirty 行 - // 延迟 5s 再触发,给 dying MN 的 in-flight flush 收敛窗口 - thdf.submit(() -> { - TimeUnit.SECONDS.sleep(5); - claimAndFlush(); - }); -} - -@Override -public void nodeJoin(ManagementNodeInventory inv) { - // 无需特殊处理,新 MN 的 Poller 正常启动即可 -} - -@Override -public void iAmDead(ManagementNodeInventory inv) { - // 本 MN 即将死亡,不做处理 - // FK SET_NULL 会自动释放本 MN 认领的行 -} - -@Override -public void iJoin(ManagementNodeInventory inv) { - // 由 managementNodeReady 启动 Poller -} -``` - -**接管延迟**:心跳超时(~30s) + nodeLeft 延迟 5s 触发 ≈ **~35 秒**。 - -**最大锁定时间分析**:dirty 行被认领后的最大锁定时间 = MN 心跳超时(默认约 60s) + Poller 间隔(默认 5s)= **~65s**。若 JVM GC pause < 60s,MN 仍存活,dirty 行在 pause 后继续处理;若 GC pause 超过心跳超时 → MN 被判定离线 → FK SET_NULL 释放认领 → 对端 MN 接管。 - -## 7.3 MN 加入场景(无影响) - -``` -T0: MN-A 独自运行,Poller 认领并处理所有 dirty 行 -T1: MN-B 加入 -T2: MN-B Poller 启动 → 与 MN-A Poller 并行运行 - → 两个 Poller 竞争认领 → DB CAS 保证互斥 → 自然负载均衡 -``` - -无需任何特殊处理。两个 Poller 天然分摊工作。 - -## 7.4 双 MN 负载分配 - -两个 MN 的 Poller 并行运行,通过 DB CAS 自然竞争: - -- CAS `UPDATE ... WHERE managementNodeUuid IS NULL LIMIT N` → 每个 MN 各抢到一部分 -- 负载分配取决于 Poller 执行时机,不保证精确 50/50 - -通常不需要精确均匀分配。如需更均匀可在 claim 查询中按 vmUuid 分片(`vmUuid % 2 = mnIndex`),但这引入了对 MN 数量的依赖,不推荐。 - -## 7.5 时序验证 - -### 正常态 - -``` -MN-A: API 成功 → markDirty(vm-1) → INSERT dirty 行 -MN-B: Poller → CAS claim → flush → 成功 → DELETE (Y) -→ 任何一个 MN 都可以处理任何 VM 的 dirty 行 (Y) -``` - -### MN 宕机 - -``` -T0: MN-A claim dirty(vm-1), 正在刷写 -T1: MN-A 宕机 -T~30: MN-B 心跳检测 → 删除 ManagementNodeVO(A) - → FK SET_NULL → dirty(vm-1).managementNodeUuid = NULL -T~35: MN-B nodeLeft(A) → 延迟 5s 后触发 claimAndFlush() - → CAS claim vm-1 → flush → 成功 → DELETE (Y) -``` - -## 7.6 Zombie MN 防护(Fence Check) - -GC pause 场景下,MN-A 可能被判定离线后又恢复执行旧任务。为避免 A/B 并发写同一 VM,在真正写 sblk 前增加认领围栏检查(QX-2): - -```java -// doFlush() 内,在发送 Agent 写请求前 -VmMetadataDirtyVO dirty = dbf.findByUuid(vmUuid, VmMetadataDirtyVO.class); -if (dirty == null || !Platform.getManagementServerId().equals(dirty.getManagementNodeUuid())) { - logger.warn("Lost claim on vm {}, abort flush write", vmUuid); - return; -} -``` - -说明:fence check 之后到实际 pwrite 之间仍存在微窗口,最终一致性由 sblk 双 Slot + `WriteSequence` 单调递增兜底(读取选择更高 SeqNum)。 - -**M2 修复 — Fence Check 强化说明**: - -Fence Check 的设计目的是**缩小** zombie MN 与接管 MN 并发写入的窗口,而非完全消除。完全消除需要分布式锁(如 etcd lease),成本不可接受。当前方案的安全性层次: - -| 防护层 | 机制 | 窗口 | -|--------|------|------| -| Layer 1 | DB CAS 认领互斥 | 正常场景下无并发 | -| Layer 2 | Fence Check(dirty 行认领验证) | 仅 GC pause 后恢复的极端场景 | -| Layer 3 | sblk 双 Slot + WriteSequence 单调递增 | 即使并发写入,读取侧选择更高 SeqNum,保证最终一致 | -| Layer 4 | `nodeLeft` 延迟(默认 5s,§13 可配) | 降低 Layer 2 场景出现概率 | - -**运维建议**:若监控发现 `Lost claim on vm` 日志频率升高,应检查 GC 配置或增大 `vm.metadata.nodeLeft.delaySec`。 - -**nodeLeft 5s + Fence Check 微窗口的残余风险分析**: -MN-A GC pause 恢复后可能在 Fence Check 通过与 pwrite 之间的微窗口内执行写入,同时 MN-B 的 nodeLeft 延迟 claimAndFlush 也在写入。此微窗口无法通过 DB CAS 消除(已脱离 DB 调度)。 -**可接受的残余风险**:sblk 双 Slot + WriteSequence 单调递增保证了即使并发写入,读取侧永远选择更高 SeqNum 的 Slot,数据最终一致。非 sblk 存储(local/NFS)使用 atomic write(tmp+fsync+rename),最后一次 rename 覆盖前者,同样最终一致。 - -### MN 加入 - -``` -T0: MN-A 独自运行,处理所有 dirty -T1: MN-B 加入 → Poller 启动 -T6: MN-A Poller: claim 3 rows → flush - MN-B Poller: claim 2 rows → flush - → 自然分摊 (Y) -``` - ---- - -# 8. 管理平面恢复策略 - -恢复策略表: - -| 触发源 | 检测方式 | 管理平面行为 | -|--------|---------|-------------| -| 刷写达到重试上限 | `onFlushFailure()` | 告警日志 + 删除 dirty 行(下次 API 自动重试) | -| read 返回 NEED_REPAIR | 巡检/读取时 | `RepairMetadataMsg`(4KB Header 写) | -| read 返回 CORRUPTED | 巡检/读取时 | `markDirty(vmUuid)`(全量重写) | -| read 返回 STORAGE_CHANGE_INCOMPLETE | 巡检/读取时 | `markDirty(vmUuid)` | -| VG 空间不足 | Agent 返回错误码 | 告警 + 退避 + 巡检重试 | -| 注册崩溃残留 | MN 启动/定时扫描 | Saga 回滚(5 条件判断) | -| 存储迁移失败 | 迁移 post-hook(`afterMigrateVmStorageFailed`) | 告警 + `markDirty(vmUuid, storageStructureChange=true)` 自愈。`storageStructureChange=true` 确保下轮 Poller 刷写时 OP type=2 (STORAGE_CHANGE),触发 sblk Agent 端重新定位 Slot。失败回滚同时执行 `deleteMetadata(targetPsUuid, vmUuid)` 清理目标端残留 + `nextRetryTime=NULL` 恢复 Poller。详见 [Part 1c §1.4](vm-metadata-01c-存储层与模板虚拟机.md#14-元数据生命周期) 失败回滚策略 | -| VM 销毁残留 | 销毁 post-hook + 巡检 | 孤儿 LV 检测 + 运维清理 | - -## 8.1 重试上限后的恢复策略 - -采用“告警 + 下次 API 触发自动重试”的简化策略,移除 MetadataStaleEvent → recovery cycle 机制,避免无限重试循环。 - -当 `retryCount >= maxRetry`(默认 5 次,约 5 分钟)时: - -1. **告警**:ERROR 日志记录 vmUuid + 失败原因 + 重试次数 -2. **删除 dirty 行**:放弃本轮重试 -3. **自然恢复**:下次该 VM 的 `@MetadataImpact` API 成功 → `markDirty()` → 全新重试(retryCount=0) - -**为什么不需要 MetadataStaleEvent 恢复机制**: - -| 方面 | 旧方案(recovery cycle) | 新方案(告警 + API 重试) | -|------|--------------------------|---------------------------| -| 复杂度 | 需 ResourceConfig 持久化 cycle 计数 + 优先队列 + 定时任务 | 无额外代码 | -| 无限循环风险 | 需 cycle 上限 + permanently stale 标记 | 不存在(只在 API 触发时重试) | -| 恢复时机 | 固定延迟 5 分钟 | 自然发生(下次 API 时) | -| PS 持续故障 | cycle 耗尽后 permanently stale | 每次 API 都重试一轮(5 次退避),不会无限堆积 | - -路径指纹巡检作为兗底方案:发现路径漂移时调用 `markDirty()` 触发全新刷写(见 §8.2)。 - -## 8.2 路径指纹巡检 — 轻量级漂移检测 - -### 8.2.1 问题:为什么不能读存储比对 - -原方案"周期性全量比对 DB vs 存储元数据"需要 agent 调用读取存储上的 sblk 文件、解码、反序列化,对每个 VM 都是一次 I/O 操作。对于大规模环境(数千 VM),这个开销不可接受。 - -### 8.2.2 思路:写时记录路径快照,读时纯 DB 比对 - -每次 Poller 刷写成功后,将本次构建元数据时用到的**所有 Volume 和 Snapshot 的 installPath** 记录到 DB。一个独立的周期巡检任务从 DB 查询当前路径,与记录的快照比对——**整个过程零存储 I/O**。 - -### 8.2.3 路径指纹结构 - -```java -@Entity -@Table(name = "VmMetadataPathFingerprintVO") -public class VmMetadataPathFingerprintVO { - @Id - @Column - @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ReferenceOption.CASCADE) - private String vmInstanceUuid; // PK, FK → VmInstanceEO (CASCADE DELETE) - - @Column - @Lob - private String pathSnapshot; // 上次刷写时的路径列表(JSON) - - @Column - private Timestamp lastFlushTime; // 记录时间 - - @Column - private boolean lastFlushFailed; // H2/M1 修复:重试耗尽时置 true,MetadataStaleRecoveryTask 重新入队后置 false - - @Column - private int staleRecoveryCount; // Q27 熔断:MetadataStaleRecoveryTask 累计重入队次数,达到上限后停止自动恢复 -} -``` - -**`lastFlushFailed` 字段说明(M1 修复)**: -- **写入时机**:Poller `onFlushFailure()` 中,当 `retryCount >= maxRetry` 时,在删除 dirty 行之前设置 `lastFlushFailed = true` -- **清除时机**:`MetadataStaleRecoveryTask`(见 [Part 2 §4.8](vm-metadata-02-脏标记与Poller.md#48-stale-恢复任务h2-修复))扫描到该行后调用 `markDirty()` 并重置为 `false` -- **默认值**:`false`(正常刷写成功时不修改此字段) -- **与 §8.1 的关系**:§8.1 的"告警 + 删除 dirty 行 + 下次 API 自动重试"策略保持不变。`lastFlushFailed` 作为补充标记,使得即使没有后续 API 触发,`MetadataStaleRecoveryTask` 也能在 30 分钟内自动发现并重新入队 - -**无限慢重试回路的熔断机制**: -当 PS 长期不可达时,`lastFlushFailed=true → StaleRecoveryTask markDirty → Poller 5 次重试 → 再次 lastFlushFailed=true → 30min 后再来` 形成无限慢循环。 -**熔断设计**:在 `VmMetadataPathFingerprintVO` 增加 `staleRecoveryCount INT DEFAULT 0` 字段,每次 `MetadataStaleRecoveryTask` 重新入队时递增。 -当 `staleRecoveryCount >= vm.metadata.staleRecovery.maxCycles`(默认 10,即约 5 小时)时,置 `lastFlushFailed=false`(停止自动重入队),并记录 WARN 日志: -`"VM [{}] metadata stale recovery exceeded {} cycles, entering permanent-stale. Use APIUpdateVmMetadataMsg to manually trigger."` -管理员可通过 `APIUpdateVmMetadataMsg` 手动触发刷写,该 API 的 `markDirty` 调用会重置 `staleRecoveryCount=0`。 -这避免了对永久不可达 PS 的无限资源消耗,同时保留了手动恢复能力。 - -`pathSnapshot` 格式(JSON,便于调试和日志输出): - -```json -{ - "volumes": [ - {"uuid": "vol-aaa", "installPath": "/dev/vg/vol-aaa"}, - {"uuid": "vol-bbb", "installPath": "/dev/vg/vol-bbb"} - ], - "snapshots": [ - {"uuid": "sp-001", "installPath": "/dev/vg/sp-001"}, - {"uuid": "sp-002", "installPath": "/dev/vg/sp-002"} - ] -} -``` - -列表按 uuid 排序,确保同样的拓扑总是产生相同的 JSON,便于字符串直接比对。 - -**JSON 字段序确定性保证**:`buildPathJson()` 使用 Gson 序列化简单内部 POJO(仅含 `uuid` 和 `installPath` 两个 String 字段),Gson 按 Java 字段声明顺序输出(非 `@SerializedName` alphabetical),声明顺序在编译后固定。列表层面按 `uuid ASC` 排序。两层确定性保证——字段声明顺序 + 列表排序——确保相同拓扑始终产生 byte-identical JSON。 - -### 8.2.4 写入时机 - -Poller 刷写成功 → `deleteRow()` 前,调用 `savePathFingerprint(vmUuid)`: - -```java -private void savePathFingerprint(String vmUuid) { - List volumes = Q.New(VolumeVO.class) - .eq(VolumeVO_.vmInstanceUuid, vmUuid) - .orderBy(VolumeVO_.uuid, SimpleQuery.Od.ASC).list(); - List snapshots = Q.New(VolumeSnapshotVO.class) - .in(VolumeSnapshotVO_.volumeUuid, volumes.stream().map(VolumeVO::getUuid).collect(toList())) - .orderBy(VolumeSnapshotVO_.uuid, SimpleQuery.Od.ASC).list(); - - VmMetadataPathFingerprintVO fp = new VmMetadataPathFingerprintVO(); - fp.setVmInstanceUuid(vmUuid); - fp.setPathSnapshot(buildPathJson(volumes, snapshots)); - fp.setLastFlushTime(new Timestamp(System.currentTimeMillis())); - dbf.insertOrUpdate(fp); -} -``` - -### 8.2.5 巡检 PeriodicTask(Keyset 分页) - -```java -public class MetadataPathDriftDetector implements PeriodicTask { - @Override - public long getInterval() { - return VmGlobalConfig.VM_METADATA_PATH_CHECK_INTERVAL.value(Long.class); - // 默认 300 秒(5 分钟) - } - - @Override - public void run() { - int batchSize = VmGlobalConfig.VM_METADATA_PATH_CHECK_BATCH_SIZE.value(Integer.class); // default 500 - String lastUuid = ""; - while (true) { - List batch = SQL.New( - "select fp from VmMetadataPathFingerprintVO fp where fp.vmInstanceUuid > :lastUuid order by fp.vmInstanceUuid asc", - VmMetadataPathFingerprintVO.class) - .param("lastUuid", lastUuid) - .limit(batchSize) - .list(); - if (batch.isEmpty()) { - break; - } - - for (VmMetadataPathFingerprintVO fp : batch) { - String currentSnapshot = buildCurrentPathSnapshot(fp.getVmInstanceUuid()); - if (!fp.getPathSnapshot().equals(currentSnapshot)) { - logger.warn("path drift detected for VM [{}], recorded: {}, current: {}", - fp.getVmInstanceUuid(), fp.getPathSnapshot(), currentSnapshot); - markDirty(fp.getVmInstanceUuid()); - } - } - - lastUuid = batch.get(batch.size() - 1).getVmInstanceUuid(); - } - } -} -``` - -**设计要求**:禁止 `dbf.listAll(VmMetadataPathFingerprintVO.class)` 全量加载。大规模环境必须使用 keyset 分页(`vmInstanceUuid > lastUuid`,因 PK 为 `vmInstanceUuid` 而非自增 `id`)。 - -**keyset 分页与非事务性间隙**:INSERT IGNORE 和后续 UUID 分页查询不在同一事务中,期间可能有新 VM 创建或旧 VM 销毁。这是可接受的:新 VM 由下轮巡检覆盖;销毁的 VM 因 FK CASCADE 自动清理 dirty 行和 fingerprint 行。不需要额外处理。 - -### 8.2.6 对比原方案 - -| | 原方案(读存储比对) | 路径指纹巡检 | -|---|---|---| -| I/O 开销 | 每 VM 一次 agent 调用读 sblk | **零存储 I/O**,纯 SQL | -| 可运行频率 | 分钟级(受 agent 吞吐限制) | 秒级(仅 DB 查询) | -| 检测范围 | 完整内容(含格式/编码差异噪声) | 仅存储拓扑路径变更(精准) | -| 首次可用 | 需 VM 已写过元数据 | 同左(需至少一次刷写记录指纹) | -| 存储拓扑变更 | 100% 检测 | 100% 检测(路径覆盖所有拓扑变更) | -| 非拓扑字段变更 | 可检测(如 size/description) | **不检测**(这些已被 `@MetadataImpact` API 覆盖) | -| 调试友好 | 需读存储 + 解码 | JSON 路径列表直接可读,drift 时日志输出新旧对比 | - -### 8.2.7 边界条件 - -| 场景 | 处理 | -|------|------| -| VM 从未刷写过元数据 | 无指纹记录 → 巡检跳过 | -| VM 已销毁 | FK CASCADE → 指纹记录自动删除 | -| 刷写成功但保存指纹前 MN 崩溃 | 指纹仍是旧的 → 下轮巡检发现 drift → markDirty → 重新刷写 + 更新指纹 | -| markDirty 已调用但尚未刷写 | 巡检发现 drift → 再次 markDirty → 幂等(dirty 行已存在,UPSERT 无副作用) | -| 并发刷写 + 巡检 | 巡检 markDirty 后 Poller 刷写覆盖 → 下轮巡检指纹一致 → 收敛 | - -## 8.3 VM 销毁时的元数据清理 - -在 `ExpungeVmInstanceFlow` 链中增加 `NoRollbackFlow` step:查找根卷所在 PS → `metadataStorageHandler.deleteMetadata()` → **best-effort**,失败仅 WARN 日志,不阻塞 VM 物理清除。 - -**删除时机说明(讨论 Δ-5)**:元数据文件的删除发生在 Expunge(物理删除)阶段而非 Destroy(软删除)阶段。Destroy 时 VM 可通过 Recover 恢复,删除元数据将导致恢复后无法自愈。Expunge 是不可逆操作,此时删除是安全的。 - -dirty 行的清理由 FK CASCADE 自动完成(VM 物理删除 → VmInstanceEO 删除 → dirty 行级联删除)。 - -**VmInstanceEO 软删除时序**:ZStack 的 VM 删除分两阶段: -1. 软删除:`VmInstanceVO` 的 `@SoftDeletionCascade` 将记录从 `VmInstanceVO` 视图移除,但底层 `VmInstanceEO` 行仍存在。此时 FK CASCADE **不触发**,dirty 行保留。若 Poller 此时认领该 VM,Part 2 §4.3 的 Destroyed 状态过滤会跳过。 -2. 物理删除:`GarbageCollectorVO` 驱动的清理任务在软删除后数分钟到数小时执行 `DELETE FROM VmInstanceEO WHERE uuid=?`,此时 FK CASCADE 触发,dirty 行 + fingerprint 行被级联删除。 -在软删除到物理删除的窗口内,dirty 行存在但被 Poller 的 Destroyed 过滤跳过,不会产生多余 flush 操作。物理删除后所有关联行自动清理。整条链路无需额外处理。 - -## 8.4 孤儿元数据检测与清理 - -### 8.4.1 孤儿产生场景 - -| 场景 | 原因 | 孤儿位置 | -|------|------|----------| -| VM 销毁时 `deleteMetadata` 失败 | Agent 超时/PS 不可用 | 元数据残留在 VM 原根盘所在 PS | -| 存储迁移崩溃([Part 1c §1.4](vm-metadata-01c-存储层与模板虚拟机.md#14-元数据生命周期) SM-02) | MN 在 Step 2 成功后、Step 8 前崩溃,且未触发回滚 | 目标 PS 上有孤儿元数据 | -| 存储迁移成功但 Step 8 清理失败 | 源 PS 删除元数据失败 | 源 PS 上残留旧元数据 | - -### 8.4.2 检测机制 — MetadataOrphanDetector - -独立 PeriodicTask,低频运行(默认每小时一次),扫描存储上的元数据并比对 DB 状态: - -```java -public class MetadataOrphanDetector implements PeriodicTask { - @Override - public long getInterval() { - return VmGlobalConfig.VM_METADATA_ORPHAN_CHECK_INTERVAL.value(Long.class); - // 默认 3600 秒(1 小时) - } - - @Override - public void run() { - // 逐 PS 扫描,复用 Scan API 的 Agent 调用 - List allPs = Q.New(PrimaryStorageVO.class) - .in(PrimaryStorageVO_.type, List.of("SharedBlock", "LocalStorage", "NFS")) - .eq(PrimaryStorageVO_.state, PrimaryStorageState.Enabled) - .list(); - - for (PrimaryStorageVO ps : allPs) { - detectOrphansOnPs(ps); - } - } - - private void detectOrphansOnPs(PrimaryStorageVO ps) { - // 1. Agent 扫描该 PS 上所有元数据条目(轻量:仅返回 vmUuid 列表) - List vmUuidsOnStorage = metadataStorageHandler.scanMetadataVmUuids(ps.getUuid()); - - for (String vmUuid : vmUuidsOnStorage) { - // 2. 检查 VM 是否存在 - VmInstanceVO vm = dbf.findByUuid(vmUuid, VmInstanceVO.class); - if (vm == null) { - // VM 已销毁 → 确认孤儿 - reportOrphan(ps.getUuid(), vmUuid, "VM_DELETED"); - continue; - } - - // 3. 检查 VM 根盘是否在此 PS 上 - String rootPsUuid = Q.New(VolumeVO.class) - .eq(VolumeVO_.vmInstanceUuid, vmUuid) - .eq(VolumeVO_.type, VolumeType.Root) - .select(VolumeVO_.primaryStorageUuid) - .findValue(); - - if (rootPsUuid != null && !rootPsUuid.equals(ps.getUuid())) { - // 根盘在其他 PS → 此 PS 上的元数据是迁移残留 - reportOrphan(ps.getUuid(), vmUuid, "ROOT_ON_OTHER_PS"); - } - } - } - - private void reportOrphan(String psUuid, String vmUuid, String reason) { - logger.warn("orphan metadata detected: ps={}, vm={}, reason={}", psUuid, vmUuid, reason); - // 记录审计日志,不自动删除(安全起见) - } -} -``` - -### 8.4.3 清理策略 - -孤儿元数据**仅报告不自动删除**,原因: -- 迁移崩溃后 MN 重启,`recoverStalledMigrationPauses()`([Part 1c §1.6](vm-metadata-01c-存储层与模板虚拟机.md#16-存储迁移-poller-暂停的崩溃恢复))重置 `nextRetryTime`,Poller 从 DB 全量重建写入正确 PS,迁移残留自然成为孤儿 -- 自动删除有误删风险(如扫描与 DB 查询之间 Root Volume 正在迁移) - -运维可通过以下方式按需清理: -1. `APICleanupVmInstanceMetadataMsg`([Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据))指定 `vmUuids` + `primaryStorageUuids` 精确清理 -2. sblk:`lvremove {vg}/{vm_uuid}_vmmeta` -3. local/NFS:`rm {mountPath}/.zstack-vm-metadata/{vm_uuid}.json` - -### 8.4.4 与 §8.3 的关系 - -| 场景 | §8.3 处理 | §8.4 兜底 | -|------|-----------|-----------| -| VM 销毁 deleteMetadata 成功 | (Y) 清理完成 | 不会检测到孤儿 | -| VM 销毁 deleteMetadata 失败 | (!) WARN 日志 | 1 小时后检测到 `VM_DELETED` 孤儿 | -| 迁移崩溃残留 | 不涉及(VM 未销毁) | 1 小时后检测到 `ROOT_ON_OTHER_PS` 孤儿 | - -## 8.5 主存储卸载/重新挂载时的元数据行为 - -| 阶段 | 行为 | -|------|------| -| PS 卸载(Detach) | Poller flush 失败(Agent 不可达),dirty 行进入 retry→stale 周期。PathFingerprint 的 `lastFlushFailed=true`。Poller 不主动清理 dirty 行,保留供后续恢复。 | -| PS 保持卸载 | StaleRecoveryTask 周期性重入队 markDirty → 5 次重试失败 → 再次 stale → 最终触发 Q27 熔断,停止自动恢复(约 5 小时后)。WARN 日志提示管理员。 | -| PS 重新挂载(Reattach) | 下一次 API 触发的 `markDirty` 或管理员手动 `APIUpdateVmMetadataMsg` 重新入队。若已熔断,`APIUpdateVmMetadataMsg` 重置 `staleRecoveryCount=0`。Poller 正常 flush 恢复。 | -| 无需特殊处理的原因 | dirty 行和 fingerprint 行在 DB 中持久化,PS 卸载不影响 DB 状态。恢复后 Poller 从 DB 全量读取构建 payload,确保元数据完整。 | - ---- - -# 9. 升级后全量刷新 - -## 9.1 触发条件 - -在 `managementNodeReady()` 回调中执行: - -1. 查询所有在线 `ManagementNodeVO`,收集 version 集合 -2. 若存在多个不同版本(滚动升级中)→ 跳过 -3. 版本唯一且与 `lastRefreshVersion`(GlobalConfig 持久化)不同 → 提交延迟 10 分钟的定时任务 -4. 10 分钟后再次检查所有 MN 版本是否一致 → 一致则执行全量刷新,不一致则跳过 -5. **recent-nodeLeft 检查(M3 修复)**:执行全量刷新前,检查最近 15 分钟内是否有 `nodeLeft` 事件。若有,说明可能仍在滚动升级过程中(旧 MN 刚下线),延迟 10 分钟后重新从步骤 1 开始检查 - -**M3 修复说明**:滚动升级的典型模式是"停旧 MN → 升级 → 启新 MN"。在这个过程中,可能出现短暂的"版本唯一"假象: -- T0:MN-A(v2) 启动,MN-B(v1) 尚未下线 → 版本不同 → 步骤 2 跳过 (Y) -- T1:MN-B(v1) 下线 → `nodeLeft` 事件 -- T2:MN-A(v2) 是唯一 MN → 版本唯一 → 步骤 3 匹配 → 提交延迟任务 -- T3(10min 后):步骤 4 检查 → 仍只有 MN-A → 版本一致 → 触发全量刷新 -- T4(但 T3+5min 后):MN-B(v2) 上线 → **此时已不需要再次全量刷新** - -问题在步骤 T3:虽然版本一致,但 MN-B 还未上线。在 MN-B 上线前执行全量刷新是正确的(它也会处理),但若 MN-B 的 `managementNodeReady()` 也触发同样逻辑,会导致**两次全量刷新**。通过 `lastRefreshVersion` 检查可避免重复(步骤 3 的 `lastRefreshVersion != currentVersion` 条件),所以实际安全。 - -但真正的风险是:升级窗口内旧版 MN 的元数据刷写可能使用旧 schema,全量刷新应确保**所有 MN 都已升级完成**。`recent-nodeLeft` 检查补充了这一保证。 - -**延迟 10 分钟的原因**:滚动升级期间,第一个 MN 升级完成时可能短暂出现"版本唯一"假象(旧 MN 尚未恢复上线)。 - -## 9.2 刷新执行(简化,无 LongJob) - -不需要 LongJob。直接批量 markDirty,Poller 自动处理。 - -```java -private void submitFullRefresh(String currentVersion) { - logger.info("metadata full refresh: starting for version {}", currentVersion); - - // Q24 修复:按 C-DM-01 要求使用 INSERT IGNORE + UPDATE 两步,不使用 ON DUPLICATE KEY - // 同时使用 keyset 分页替代 OFFSET,避免大数据集性能退化 - int batchSize = VmGlobalConfig.VM_METADATA_UPGRADE_REFRESH_BATCH_SIZE.value(Integer.class); // default 1000 - String lastUuid = ""; - int totalProcessed = 0; - - while (true) { - // Step 1: INSERT IGNORE — 为尚无 dirty 行的 VM 创建新行 - // storageStructureChange=true(C-SC-07:升级后无法判断存储拓扑是否变化,保守使用 STORAGE 级别) - int inserted = SQL.New( - "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + - "SELECT v.uuid, 1, 1 FROM VmInstanceVO v " + - "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + - "ORDER BY v.uuid ASC LIMIT :batchSize") - .param("lastUuid", lastUuid) - .param("batchSize", batchSize) - .execute(); - - // Step 2: UPDATE — 已有 dirty 行的 VM 递增 dirtyVersion + 升级 storageStructureChange - SQL.New( - "UPDATE VmMetadataDirtyVO d " + - "INNER JOIN VmInstanceVO v ON d.vmInstanceUuid = v.uuid " + - "SET d.dirtyVersion = d.dirtyVersion + 1, " + - " d.storageStructureChange = 1 " + - "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + - "ORDER BY v.uuid ASC LIMIT :batchSize") - .param("lastUuid", lastUuid) - .param("batchSize", batchSize) - .execute(); - - // 更新 lastUuid 用于 keyset 分页 - List batch = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + - "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + - "ORDER BY v.uuid ASC LIMIT :batchSize", String.class) - .param("lastUuid", lastUuid) - .param("batchSize", batchSize) - .list(); - - if (batch.isEmpty()) { - break; - } - - totalProcessed += batch.size(); - lastUuid = batch.get(batch.size() - 1); - } - - logger.info("metadata full refresh: {} VMs processed for version {}", totalProcessed, currentVersion); - - // Poller 自动分批处理,ChainTask 自动限流 - - // 更新 lastRefreshVersion — 必须在全量刷新完成后写入(讨论 Δ-8) - // 不得在刷新开始前写入:若刷新过程中 MN 崩溃,提前写入会导致重启后 - // lastRefreshVersion 已等于 currentVersion,跳过本次刷新,遗留未处理的 VM。 - // 写在完成后:崩溃重启 → lastRefreshVersion 仍为旧值 → 重新触发全量刷新 → 幂等安全。 - VmGlobalConfig.VM_METADATA_LAST_REFRESH_VERSION.updateValue(currentVersion); -} -``` - -**说明**:原实现使用 `INSERT ... ON DUPLICATE KEY UPDATE` 单条 SQL,与 C-DM-01 约束(禁用 ON DUPLICATE KEY,避免 Galera 死锁)不一致。改为 `INSERT IGNORE + UPDATE` 两步语义,与 `markDirty()` 保持统一。同时将 `OFFSET` 分页改为 keyset 分页(`uuid > :lastUuid`),与 §9a.1 和 §8.2.5 保持一致,避免大数据集性能退化。 - -**storageStructureChange=true 已修正**:与 C-SC-07 约束对齐——升级全量刷新场景中无法判断存储拓扑是否变化,保守使用 `storageStructureChange=true`(原实现误设为 0)。 - -**为什么用分批批量 SQL 替代逐个 markDirty**:万级 VM 环境中,逐个 INSERT 产生万级 SQL 语句;单条超大批量 SQL 又可能超时。按 1000 行分批可在吞吐与稳定性间平衡。 - ---- - -# 9a. 功能开关切换处理 - -## 9a.1 `false → true`(启用)— 分批全量初始化 - -通过 `GlobalConfig.installUpdateExtension` 监听 `vm.metadata.enabled` 变更。检测到 `false → true` 时,提交分批初始化任务,为所有尚无元数据(无 dirty 行也无 PathFingerprint 记录)的 UserVm 创建 dirty 行。 - -**核心设计:防止读写风暴** - -与升级全量刷新(§9.2)不同,开关启用可能在业务高峰时执行。直接批量 INSERT 大量 dirty 行后 Poller 瞬间看到全部可认领行,可能引发存储 IO 风暴。因此引入**批间延迟**: - -```java -private void submitBatchInitialization() { - thdf.submit(new Task(null) { - @Override - public Void call() { - if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { - // 延迟执行前再次检查,防止快速 toggle 后仍执行初始化 - logger.info("vm.metadata.enabled toggled back to false before initialization, skip"); - return null; - } - - int batchSize = VmGlobalConfig.VM_METADATA_INIT_BATCH_SIZE.value(Integer.class); // default 200 - long batchDelaySec = VmGlobalConfig.VM_METADATA_INIT_BATCH_DELAY_SEC.value(Long.class); // default 5 - String lastUuid = ""; - int totalInitialized = 0; - - while (true) { - // 每轮检查开关状态,若已关闭则中止 - if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { - logger.info("vm.metadata.enabled disabled during initialization, abort. initialized={}", - totalInitialized); - break; - } - - // Keyset 分页查询尚无 dirty 行的 UserVm - int initialized = SQL.New( - "INSERT IGNORE INTO VmMetadataDirtyVO (vmInstanceUuid, dirtyVersion, storageStructureChange) " + - "SELECT v.uuid, 1, 0 FROM VmInstanceVO v " + - "LEFT JOIN VmMetadataDirtyVO d ON v.uuid = d.vmInstanceUuid " + - "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid AND d.vmInstanceUuid IS NULL " + - "ORDER BY v.uuid ASC LIMIT :batchSize") - .param("lastUuid", lastUuid) - .param("batchSize", batchSize) - .execute(); - - // Q29 修复:移除 `if (initialized == 0) break;`——当本批所有 VM 都已有 dirty 行时 - // INSERT IGNORE affected_rows=0,但后续批次可能还有未初始化的 VM。 - // 终止条件改为 batchUuids.isEmpty()(见下方),确保真正遍历完全部 VM。 - - totalInitialized += initialized; - - // 更新 lastUuid 用于 keyset 分页 - // Q29 — lastUuid 必须独立推进:当 INSERT IGNORE affected_rows=0(本批 VM 都已有 dirty 行) - // 时 initialized==0,但 while 循环不能终止——后续批次可能还有未初始化的 VM。 - // lastUuid 基于 VmInstanceVO 全量 UUID 推进,而非 INSERT 结果。 - List batchUuids = SQL.New("SELECT v.uuid FROM VmInstanceVO v " + - "WHERE v.type = 'UserVm' AND v.uuid > :lastUuid " + - "ORDER BY v.uuid ASC", String.class) - .param("lastUuid", lastUuid) - .limit(batchSize) - .list(); - - if (batchUuids.isEmpty()) { - break; // 真正遍历完所有 VM - } - lastUuid = batchUuids.get(batchUuids.size() - 1); - - logger.info("metadata initialization batch completed: {} VMs in this batch, {} total", - initialized, totalInitialized); - - // 批间延迟:等待 Poller 消化已有 dirty 行,避免瞬间堆积 - if (batchDelaySec > 0) { - try { - TimeUnit.SECONDS.sleep(batchDelaySec); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("metadata initialization interrupted"); - break; - } - } - } - - logger.info("metadata initialization complete: {} VMs total", totalInitialized); - return null; - } - }, Duration.ofSeconds(30)); // 延迟 30s 启动,等待 Poller 就绪 -} -``` - -**关键设计点**: - -| 设计点 | 决策 | 原因 | -|--------|------|------| -| 使用 `INSERT IGNORE` | 跳过已有 dirty 行的 VM | 幂等:重复触发不产生副作用 | -| `LEFT JOIN` 排除已有 dirty 行 | 仅为从未标脏的 VM 初始化 | 避免对已有 Poller 处理中的 VM 产生干扰 | -| Keyset 分页(`uuid > lastUuid`) | 避免 `OFFSET` 在大数据集上的性能退化 | 与路径指纹巡检(§8.2.5)保持一致 | -| 批间延迟(默认 5s) | 给 Poller 消化已提交 dirty 行的时间窗口 | 防止 dirty 行瞬间堆积万级,触发存储 IO 风暴 | -| 每轮重新检查 `vm.metadata.enabled` | 快速 toggle(开→关→开)场景下及时中止 | 防御性设计 | -| 延迟 30s 启动 | 等 Poller、ChainTask 线程池初始化完成 | `false→true` 可能在 MN 启动时通过 GlobalConfig 变更触发 | -| `storageStructureChange=0` | 首次初始化不涉及存储拓扑变更 | 用 CONFIG 级别即可,不需 STORAGE 级重建 | - -**初始化进度可观测**: - -- 日志输出每批和总计数 -- Poller 的 `vm_metadata_dirty_queue_size` Gauge(§14)自然反映待处理积压量 -- 运维可通过 `SELECT COUNT(*) FROM VmMetadataDirtyVO WHERE managementNodeUuid IS NULL` 查看剩余量 - -**与 §9.2 升级全量刷新的关系**: - -| 维度 | §9 升级全量刷新 | §9a 开关启用初始化 | -|------|----------------|-------------------| -| 触发时机 | MN 升级后自动触发 | GlobalConfig 从 false 切换到 true | -| 涉及 VM 范围 | 所有 UserVm(含已有 dirty 行的) | 仅尚无 dirty 行的 UserVm | -| SQL 策略 | `INSERT ... ON DUPLICATE KEY UPDATE`(保证已有行也递增版本) | `INSERT IGNORE ... LEFT JOIN`(仅初始化新行) | -| 批间延迟 | 无(升级窗口通常业务量低) | 有(默认 5s,防止高峰期风暴) | -| 去重保护 | `lastRefreshVersion` 检查避免重复触发 | `LEFT JOIN` + `INSERT IGNORE` 天然幂等 | - -## 9a.2 `true → false`(禁用)— 保留已有元数据,按需清理 - -关闭 `vm.metadata.enabled` 后: - -1. **Poller 自动停止处理**:`markDirty()` 和 `triggerFlushForVm()` 的前置检查直接 return,不再产生新的标脏和刷写 -2. **清理 PathFingerprint 记录(讨论 Δ-10)**:异步批量删除所有 `VmMetadataPathFingerprintVO` 行。原因:功能关闭期间存储拓扑可能发生变更(卷迁移、快照删除等),重新启用时旧指纹与实际拓扑不一致,会导致路径巡检(§8.2)产生大量误报 drift。清理采用 keyset 分页异步删除(每批 1000 行),不阻塞 GlobalConfig 变更回调。dirty 行的 FK CASCADE 不受影响。 -3. **已有 dirty 行保留**:不主动清理 `VmMetadataDirtyVO` 表中的残留行。原因: - - 若运维快速重新开启(toggle back),残留行可立即被 Poller 消费 - - 若长期关闭,残留行占用 DB 空间可忽略(每行 < 200 bytes) -4. **存储上的元数据文件/LV 保留**:不自动删除已持久化的元数据。理由: - - 防止误操作导致已有容灾数据丢失 - - 元数据文件体积小(通常 < 500KB/VM),即使保留也不构成存储压力 - - 运维需要时可通过 `APICleanupVmInstanceMetadataMsg` 按需清理 - -**`APICleanupVmInstanceMetadataMsg`** 详见 [Part 5 §6.3](vm-metadata-05-API设计.md#63-清理虚拟机元数据)。核心设计: - -- 可按 `primaryStorageUuids`(指定 PS)或 `vmUuids`(指定 VM)粒度清理 -- 不传参数则清理**所有已停用(`vm.metadata.enabled=false`)时的全量数据** -- 清理操作为 `deleteMetadata` + 删除 `VmMetadataPathFingerprintVO` + 删除残留 `VmMetadataDirtyVO` -- 清理操作幂等:重复调用不报错 - -**安全约束**:`APICleanupVmInstanceMetadataMsg` 在 `vm.metadata.enabled=true` 时**拒绝执行**(错误码 `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`),防止在功能启用状态下误清理正在使用的元数据。仅当功能关闭后才允许执行。 - ---- - -# 10. Payload 大小保护 - -在 `VmInstanceBase.doHandleUpdateVmInstanceMetadata()` 中,`buildVmInstanceMetadata()` 构建 payload 后进行大小检查: - -| 阈值 | 行为 | 说明 | -|------|------|------| -| > 8MB | WARN 日志 | 早期预警,提示运维关注 | -| > 30MB | ERROR + 拒绝写入 + reply 错误 | 保护 sblk LV 空间 | - -正常 VM 的 metadata payload 通常在 10KB~500KB 范围内。超过 8MB 几乎一定表示异常(如快照未清理导致数千条记录)。 - -### 10.0 容量公式与常量(QX-8) - -```java -public final class VmMetadataConstants { - public static final long SBLK_HEADER_SIZE = 4096L; - public static final long SBLK_SLOT_HEADER_SIZE = 36L; - public static final long SBLK_MAX_LV_SIZE = 64L * 1024 * 1024; - - public static long slotCapacity(long lvSize) { - return ((lvSize - SBLK_HEADER_SIZE) / 2 / 4096) * 4096; - } - - public static final long SBLK_MAX_SLOT_CAPACITY = slotCapacity(SBLK_MAX_LV_SIZE); // 33,550,336 - public static final long SBLK_MAX_PAYLOAD_SIZE = SBLK_MAX_SLOT_CAPACITY - SBLK_SLOT_HEADER_SIZE; // 33,550,300 - public static final long PAYLOAD_WARN_THRESHOLD = 8L * 1024 * 1024; - public static final long PAYLOAD_REJECT_THRESHOLD = 30L * 1024 * 1024; -} -``` - -推导:64MB LV 下单 Slot 容量约 32MB;扣除 Slot Header(36 字节:Magic 4B + SeqNum 8B + SlotOffset 8B + SlotCapacity 8B + PayloadLen 8B)后可用 payload 约 31.99MB。30MB 阈值为显式保守余量。 - -### 10.1 Payload 大小与 sblk LV 大小映射 - -| Payload 大小范围 | LV 大小 | 典型场景 | -|-------------------|---------|----------| -| 0 ~ 2MB | 4MB(初始) | 普通 VM,1~5 个卷,少量快照 | -| 2MB ~ 4MB | 8MB | 多卷 VM,数十个快照 | -| 4MB ~ 8MB | 16MB | 大量快照的 VM | -| 8MB ~ 16MB | 32MB | 异常场景(WARN) | -| 16MB ~ 30MB | 64MB(上限) | 极端异常 | -| > 30MB | 拒绝写入 | 拒绝以保护存储 | - -LV 初始 4MB,每次扩容翻倍,最大 64MB。扩容通过 `lvextend` 完成,详见 [Part 4e §2](vm-metadata-04e-sblk运维与IO.md#2-扩容)。 - -### 10.2 写入前运行时容量校验 - -`doFlush()` 必须基于**当前 LV 实际大小**执行动态容量校验,禁止仅依据静态 30MB 阈值: - -```java -long lvSize = sblkAgent.getLvSize(psUuid, vmUuid); -long slotCap = VmMetadataConstants.slotCapacity(lvSize); -long currentPayloadCap = slotCap - VmMetadataConstants.SBLK_SLOT_HEADER_SIZE; - -if (payloadSize > currentPayloadCap) { - sblkAgent.expandLv(psUuid, vmUuid); - long newLvSize = sblkAgent.getLvSize(psUuid, vmUuid); - long newPayloadCap = VmMetadataConstants.slotCapacity(newLvSize) - VmMetadataConstants.SBLK_SLOT_HEADER_SIZE; - if (payloadSize > newPayloadCap) { - throw new PayloadTooLargeException(String.format( - "payload=%d exceeds slot capacity=%d after expand, lvSize=%d", payloadSize, newPayloadCap, newLvSize)); - } -} -``` - -若扩容后仍不足(通常达到 64MB 上限),返回明确错误码 `VM_METADATA_PAYLOAD_TOO_LARGE`,不得将底层 IO 错误透传为通用失败。 - ---- - -# 11. 潜在代价与 tradeoff - -| 代价 | 说明 | 缓解 | -|------|------|------| -| Poller 空转 | 无 dirty 行时每 5s 执行一次 SELECT → 0 rows | 开销极小(一次空查询 <1ms),可接受 | -| 双 MN 负载不均 | 两个 Poller 竞争认领,不保证 50/50 | 最终一致性保证所有行都会被处理 | -| 新增一张 DB 表 | VmMetadataDirtyVO | 结构简单,维护成本低 | -| 退避期间 Poller 查到但跳过 | nextRetryTime 尚未到 → WHERE 条件排除 | 索引命中,开销可忽略 | - ---- - -# 12. 开发约束清单 - -## 12.1 API 标注约束 - -| # | 约束 | 原因 | 违反后果 | -|---|------|------|----------| -| A1 | 新增影响 VM 元数据的 API **必须**标注 `@MetadataImpact(Impact.CONFIG)` 或 `@MetadataImpact(Impact.STORAGE)` | 拦截器仅扫描带注解的 API 类 | 该 API 的变更不会触发元数据更新,存储侧数据过期 | -| A2 | 明确不影响元数据的 API **应当**标注 `@MetadataImpact(Impact.NONE)` | Opt-out 显式声明,利于 Code Review 审查覆盖率 | 无功能影响,但降低可审计性 | -| A3 | 涉及存储拓扑变更的 API(快照/迁移/删盘)必须使用 `Impact.STORAGE`,不可用 `Impact.CONFIG` | STORAGE 下发 OP type=2 通知 Agent 处理存储拓扑变更 | Agent 不执行存储拓扑处理,sblk 场景可能数据不一致 | -| A4 | `updateOnFailure=true` 仅用于可能部分成功的 API(如批量操作) | 默认 false:失败跳过;设为 true 时失败也 markDirty | 滥用会导致失败 API 也触发无意义的全量刷写 | - -## 12.2 VM UUID 解析约束 - -| # | 约束 | 原因 | 违反后果 | -|---|------|------|----------| -| B1 | 非 VM 直接 API(如 Volume/Nic/Tag API)必须有 `VmUuidFromApiResolver` 能够处理 | 默认 Resolver 链仅覆盖 `VmInstanceMessage`/`VolumeMessage`/Tag API + 反射兜底 | 相关 VM 不会被 markDirty,元数据不更新 | -| B2 | Resolver 的 `resolveVmUuids()` 必须在 **API 执行前**调用(`beforeDeliveryMessage` 阶段) | API 执行后资源可能已删除(如 APIDeleteVolumeMsg → VolumeVO 不存在) | 无法查到关联 VM,markDirty 丢失 | -| B3 | 新增资源类型关联 VM 时,需在 `ResourceBasedVmUuidFromApiResolver.resolveByResourceType()` 中补充映射 | 当前仅覆盖 VmInstanceVO/VolumeVO/VmNicVO/VolumeSnapshotVO | Tag 操作目标为新资源类型时不触发元数据更新 | - -## 12.3 元数据构建约束 - -| # | 约束 | 原因 | 违反后果 | -|---|------|------|----------| -| C1 | `buildVmInstanceMetadata()` 必须保留在 `VmMetadataBuilder` 中并标注 `@Transactional(readOnly=true)` | 6+ 条 SELECT 需在同一 REPEATABLE READ 快照内执行 | 读到跨快照不一致数据(如 Volume 存在但其 Snapshot 已被并发删除) | -| C2 | 新增元数据字段时,需同步更新 `VmInstanceMetadataDTO` 和 `VmMetadataBuilder` | DTO 是 payload 的唯一 schema 定义 | 字段不在 DTO 中则不会序列化到 payload | -| C3 | `ResourceMetadata` 中 `systemTags`/`resourceConfigs` 字段必须为 `String`(Base64 编码),不是 `List` | 编码管线:VO 列表 → JSON 序列化 → Base64 → 单 String | 类型不匹配导致序列化异常 | - -## 12.4 标脏与刷写约束 - -| # | 约束 | 原因 | 违反后果 | -|---|------|------|----------| -| D1 | 修改 VM 存储拓扑的**内部消息** handler 必须手动调用 `markDirty()` | 非 API 操作不经过 `VmMetadataUpdateInterceptor` | 变更后元数据不更新 | -| D2 | Handler 端写入失败时**不得**调用 `markDirty()`,必须 reply error 由上层重试 | Dirty 行已存在且由 Poller 管理 retryCount 和退避 | markDirty 重置 retryCount,绕过退避机制,可能无限快速重试 | - -**D2 例外:存储迁移失败**(§8 恢复策略表)中 `afterMigrateVmStorageFailed` 调用 `markDirty(vmUuid, storageStructureChange=true)` 不违反 D2。原因:存储迁移失败的回滚会改变 installPath(从 target PS 回退到 source PS),旧 dirty 行中缓存的 installPath 指向已回滚的 target 路径,已不正确。此时必须重新 markDirty 以反映回滚后的 source-side 拓扑。这是 D2 的唯一显式例外。同时,回滚操作 `deleteMetadata(targetPsUuid) + nextRetryTime=NULL` 确保不会产生无限快速重试(retryCount 因是新 dirty 行而从 0 开始,退避机制正常生效)。 -| D3 | Agent 端写入必须幂等(全量覆盖,不做增量 merge) | 同一 VM 的并发刷写(跨 MN 极端场景)最终应收敛到一致状态 | 增量 merge 可能导致数据残留或顺序依赖 | -| D4 | `exceedMaxPendingCallback` 中**必须**执行 `globalFlushInFlight.decrementAndGet()` + `releaseClaim()` | **讨论 Δ-1 更新**:采用单层 per-VM ChainTask + AtomicInteger 全局计数器后,exceedMaxPendingCallback 表示该 VM 已有 pending 任务排队,当前提交被拒绝。此时必须归还全局计数器配额并释放 DB 认领,让 Poller 下轮重新处理。旧约束(不得释放)基于嵌套 ChainTask 设计,已不适用 | 计数器泄漏导致全局并发配额耗尽,dirty 行被永久锁定 | -| D5 | `markDirty(vmUuid, storageStructureChange)` 中 `storageStructureChange` 必须保持 OR 语义(true 一旦出现即保持 true 至该行删除) | 保守策略确保任一存储拓扑变更最终走 `STORAGE_CHANGE` 写入路径(intentional conservative behavior) | 若改为覆盖语义,可能把真实拓扑变更降级为 CONFIG 路径 | - -#### D1 补充说明 — 内部消息 handler 遗漏 `markDirty()` 的补救 - -**为什么会遗漏**:`@MetadataImpact` 注解 + CI 检查仅覆盖 `APIMessage` 子类。内部消息(如 HA handler、级联删除、定时清理等)不经过 `VmMetadataUpdateInterceptor`,CI 无法自动检测是否遗漏了 `markDirty()` 调用。 - -**补救手段——分三层**: - -| 层次 | 手段 | 时效 | 说明 | -|------|------|------|------| -| 即时修复 | `APIUpdateVmMetadataMsg` | 秒级 | 运维/CLI 手动触发指定 VM 的全量元数据刷新(见 [Part 5 §6.1](vm-metadata-05-API设计.md#61-手动触发元数据更新)) | -| 批量修复 | `APICheckVmInstanceMetadataConsistencyMsg` | 分钟级 | 一致性检查发现 DB 与存储元数据不一致时自动 `markDirty()`(见 [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性)) | -| 长期防御 | 路径指纹巡检 | 分钟级 | 每次刷写成功后记录路径快照,独立 PeriodicTask 纯 DB 比对检测漂移,不一致则自动 `markDirty()`(见 §8.2) | - -**根因修复流程**: - -``` -发现元数据滞后(运维报告 / 一致性检查告警) - │ - ├─ 1. 定位遗漏的内部消息 handler - │ - 查看该 VM 近期操作日志,找到触发存储变更但未更新元数据的内部操作 - │ - 在对应 handler 的成功回调中补充 markDirty(vmUuid) 调用 - │ - ├─ 2. 即时修复受影响的 VM - │ - CLI: `UpdateVmMetadata uuid=` - │ - 或批量: `CheckVmInstanceMetadataConsistency` - │ - └─ 3. 代码评审防护 - - 涉及 Volume/Snapshot/installPath 变更的内部消息 handler 代码评审时 - 必须检查是否调用了 markDirty() - - 评审 checklist 模板中增加 "元数据标脏" 检查项 -``` - -**注意**:即使存在遗漏,全量覆盖写语义保证了补救时的正确性——任何时刻调用 `markDirty()` 都会触发从 DB 重新构建完整元数据并覆盖写入,不存在增量丢失问题。遗漏的影响是元数据**暂时落后于 DB**,而非**永久损坏**。 - -## 12.5 并发与线程约束 - -| # | 约束 | 原因 | 违反后果 | -|---|------|------|----------| -| E1 | `nodeLeft()`/`nodeJoined()` 回调中的 DB 操作必须通过 `thdf.submit()` 异步执行 | 回调在心跳检测线程上,阻塞会影响其他 MN 状态检测 | 心跳超时导致误判 MN 离线 | -| E2 | per-VM ChainTask(`metadata-dirty-flush-vm-{uuid}`)的 `maxPending=1`,不得修改 | 确保同一 VM 最多排队 1 个 pending 任务(+ 1 running) | pending 过多导致重复提交堆积 | -| E3 | 外层全局队列 `syncLevel` 和 Layer 3 per-PS 队列 `syncLevel` 的调整需评估 DB 连接池和 Agent 并发承受力 | 二者嵌套:全局水位 × per-PS 水位 决定实际并发 | 过大导致 DB/Agent 过载,过小导致刷写积压 | - ---- - -# 13. GlobalConfig 配置项汇总 - -| 配置项 | 类型 | 默认值 | 说明 | 章节 | -|--------|------|--------|------|------| -| `vm.metadata.enabled` | Boolean | false | 元数据功能总开关 | §1 | -| `vm.metadata.dirty.pollIntervalSec` | Long | 5 | Poller 轮询间隔(秒),可动态调整 | §4.1 | -| `vm.metadata.dirty.batchSize` | Integer | 50 | 每轮 Poller 最多认领行数 | §4.2 | -| `vm.metadata.maxRetry` | Integer | 5 | 最大重试次数(达上限后告警 + 删除,下次 API 自动重试) | §4.6 | -| `vm.metadata.ps.maxConcurrent` | Integer | 5 | 同一 MN 同一 PS 最大并发写入 | §6.1 | -| `vm.metadata.global.maxConcurrent` | Integer | 10 | 同一 MN 最大并发 VM 更新数 | §6.2 | -| `vm.metadata.pathCheck.intervalSec` | Long | 300 | 路径指纹巡检间隔(秒) | §8.2 | -| `vm.metadata.pathCheck.batchSize` | Integer | 500 | 路径指纹巡检 keyset 分页批次 | §8.2.5 | -| `vm.metadata.upgrade.refreshDelaySec` | Long | 600 | 升级后全量刷新延迟时间(秒),等待滚动升级完成 | §9.1 | -| `vm.metadata.upgrade.refreshBatchSize` | Integer | 1000 | 升级全量刷新分批 SQL 批次大小 | §9.2 | -| `vm.metadata.nodeLeft.delaySec` | Long | 5 | nodeLeft 后延迟接管窗口,降低 zombie MN 竞态 | §7.2 | -| `vm.metadata.staleRecovery.intervalSec` | Long | 1800 | MetadataStaleRecoveryTask 扫描间隔(秒)(H2 修复) | §4.8 (Part 2) | -| `vm.metadata.staleRecovery.batchSize` | Integer | 100 | MetadataStaleRecoveryTask 每批扫描行数(H2 修复) | §4.8 (Part 2) | -| `vm.metadata.staleRecovery.maxCycles` | Integer | 10 | 单 VM 连续 stale recovery 熔断阈值,超过后停止自动恢复 | §8.2.3 | -| `vm.metadata.pendingApi.timeoutMinutes` | Long | 45 | pendingApis 超时清理阈值(分钟)(M4 修复) | §1.7 (Part 1b) | -| `vm.metadata.retry.baseDelaySeconds` | Integer | 10 | 指数退避基础延迟(秒) | §4.6 (Part 2) | -| `vm.metadata.retry.maxExponent` | Integer | 10 | 指数退避最大指数 | §4.6 (Part 2) | -| `vm.metadata.init.batchSize` | Integer | 200 | `false→true` 启用初始化每批 VM 数量 | §9a.1 | -| `vm.metadata.init.batchDelaySec` | Long | 5 | `false→true` 启用初始化批间延迟(秒),防止 IO 风暴 | §9a.1 | -| `vm.metadata.orphanCheck.intervalSec` | Long | 3600 | 孤儿元数据检测间隔(秒) | §8.4.2 | -| `vm.metadata.zombieClaim.thresholdMinutes` | Long | 15 | 僵尸 claim 判定阈值(分钟):`lastClaimTime` 超过此时长的已认领 dirty 行视为僵尸,`cleanupZombieClaims()` 释放其认领 | §4.8 (Part 2), C-CL-02 | -| `vm.metadata.staleClaim.thresholdMinutes` | Long | 30 | `MetadataStaleRecoveryTask` 后台扫描的过期 claim 检测阈值(分钟):`managementNodeUuid IS NOT NULL` 且 `lastClaimTime` 超过此时长的行被强制释放并重新入队。**注意**:此阈值仅用于后台周期任务,与 `triggerFlush.staleMinutes`(API 热路径)不同 | §4.8 (Part 2) | -| `vm.metadata.triggerFlush.staleMinutes` | Long | 10 | `triggerFlushForVm()` 内联 stale claim 接管阈值(分钟):API 热路径中,若 dirty 行的 `lastClaimTime` 超过此时长且认领 MN 与传入的 `staleId` 一致,允许当前 MN 接管。与 `staleClaim.thresholdMinutes`(后台扫描 30 min)形成两级保护,详见 DP-06 | §3.1 (Part 2) | -| `vm.metadata.delete.maxRetry` | Integer | 3 | `deleteMetadata` 同步重试最大次数(ExpungeVmInstanceFlow 中使用) | §2 (Part 1c) | -| `vm.metadata.delete.baseDelaySec` | Long | 30 | `deleteMetadata` 同步重试基础延迟(秒),退避公式 `baseDelay × 2^retryIndex` | §2 (Part 1c) | -| `vm.metadata.lastRefreshVersion` | String | _(内部)_ | 升级全量刷新去重标记:记录最近一次已完成的升级刷新版本号,避免双 MN 重复触发。**仅供内部使用,运维不应手动修改** | §9 | - ---- - -# 14. 可观测性指标 - -以下指标建议通过 ZStack 内置 Prometheus 埋点暴露,供 Grafana 看板使用。 - -| 指标名 | 类型 | 标签 | 说明 | -|----------|------|------|------| -| `vm_metadata_flush_total` | Counter | status={success,fail,skip} | 刷写总次数,按结果分类 | -| `vm_metadata_flush_duration_seconds` | Histogram | — | 单次刷写耗时(从 buildMetadata 到 Agent 返回) | -| `vm_metadata_dirty_queue_size` | Gauge | — | 当前未认领的 dirty 行数(每轮 Poller 统计) | -| `vm_metadata_poller_cycle_duration_seconds` | Histogram | — | 单轮 Poller 执行总耗时(含认领 + 提交) | -| `vm_metadata_registration_total` | Counter | status={success,fail,rollback} | 注册总次数 | -| `vm_metadata_retry_count` | Histogram | — | 每次刷写成功时的累计重试次数分布 | - -**告警规则建议**: -- `vm_metadata_dirty_queue_size > 500 持续 5 分钟` → WARN(刷写积压) -- `rate(vm_metadata_flush_total{status="fail"}[5m]) > 10` → WARN(批量失败) -- `vm_metadata_flush_duration_seconds{quantile="0.99"} > 30` → WARN(单次刷写太慢) - ---- - -# 15. 约束与不変量 - -| 约束 ID | 内容 | 来源章节 | -|---------|------|----------| -| C-02B-1 | `nodeLeft()` 接管必须延迟 5s 后触发 `claimAndFlush()`,不得立即抢占 | §7.2 | -| C-02B-2 | 执行 sblk 写入前必须校验 dirty 行 `managementNodeUuid == 本 MN`,失去认领立即放弃写入 | §7.6 | -| C-02B-3 | 路径巡检禁止 `dbf.listAll` 全量加载,必须采用 keyset 分页(`vmInstanceUuid > lastUuid`) | §8.2.5 | -| C-02B-4 | 升级全量刷新必须按批(默认 1000)执行批量 SQL,避免单次超大事务 | §9.2 | -| C-02B-5 | payload 上限必须同时满足静态阈值(30MB)与运行时 slot 容量校验,容量不足先扩容再写入 | §10.0, §10.2 | -| C-02B-6 | `storageStructureChange` 标志保持 OR 语义,直到 dirty 行成功删除前不得降级为 false | §12.4 | -| C-02B-7 | 容量计算常量(Header/SlotHeader/MAX_LV)必须集中定义并用于公式推导,禁止硬编码散落 | §10.0 | -| C-02B-8 | `VmMetadataPathFingerprintVO.lastFlushFailed` 仅在重试耗尽时置 true,仅由 `MetadataStaleRecoveryTask` 重置为 false,其他路径不得修改 | §8.2.3 (M1 修复) | -| C-02B-9 | 升级全量刷新执行前必须检查最近 15 分钟内无 `nodeLeft` 事件,否则延迟重试 | §9.1 (M3 修复) | -| C-02B-10 | `nodeLeft` 延迟(`vm.metadata.nodeLeft.delaySec`)调整需与 Fence Check 机制配合评估,不得单独修改 | §7.2, §7.6 (M2 修复) | -| C-02B-11 | `false→true` 初始化必须使用分批 + 批间延迟,禁止一次性全量 INSERT dirty 行 | §9a.1 | -| C-02B-12 | `APICleanupVmInstanceMetadataMsg` 必须在 `vm.metadata.enabled=false` 时才允许执行,`true` 时拒绝 | §9a.2, Part 5 §6.3 | -| C-02B-13 | `false→true` 初始化任务每批必须重新检查 `vm.metadata.enabled` 开关状态,关闭时立即中止 | §9a.1 | -| C-02B-14 | 孤儿元数据检测仅报告不自动删除,避免与进行中的存储迁移竞态导致误删 | §8.4.3 | diff --git "a/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" "b/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" deleted file mode 100644 index 3b7cfe09520..00000000000 --- "a/docs/design/vm-metadata-03-\346\263\250\345\206\214\344\270\216\350\277\220\347\273\264.md" +++ /dev/null @@ -1,572 +0,0 @@ -# VM 元数据 — 注册与运维 - -## 目录 - -1. [注册字段处理矩阵](#1-注册字段处理矩阵) -2. [跨存储数据盘处理规则](#2-跨存储数据盘处理规则) -3. [注册虚拟机详细流程](#3-注册虚拟机详细流程) -4. [注册事务回滚](#4-注册事务回滚) -5. [注册场景问题分析](#5-注册场景问题分析) -6. [可观测性](#6-可观测性) -7. [设计决策汇总](#7-设计决策汇总) -8. [运维指南:注册失败后的清理](#8-运维指南注册失败后的清理) -9. [约束与不変量](#9-约束与不変量) - -**API 定义**(请求/响应/错误码)统一见 [Part 5: API 设计](vm-metadata-05-API设计.md)。本文档不重复定义 API 结构。 - -## 0. 依赖声明 - -| 依赖项 | 类型 | 来源 | 本文使用方式 | -|--------|------|------|-------------| -| `VmMetadataPathFingerprintVO.vmInstanceUuid` | 数据模型约束 | [Part 2b §1](vm-metadata-02b-高可用与运维.md#1-高可用策略) | 字符串 UUID 作为稳定锚点,支持 keyset 分页与跨环境映射 | -| sblk 读取状态语义(OK/NEED_REPAIR/RECOVERED/DEGRADED/CORRUPTED) | 读取契约 | [Part 4d §2.4](vm-metadata-04d-sblk读取与恢复.md#24-readresult-状态语义) | 注册前读取元数据与注册后校验的可用性判定 | -| `APICheckVmInstanceMetadataConsistencyMsg` | 运维 API | [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性) | 注册完成后的一致性复核与告警触发 | - ---- - -## 1. 注册字段处理矩阵 - -### 1.1 VmInstanceVO - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| uuid | 保留 | 冲突时拒绝注册 | -| name | 保留 | — | -| description | 保留 | — | -| zoneUuid | API 参数 | 必填 | -| clusterUuid | API 参数 | 必填,赋值到 VO | -| hostUuid | 设 null | 注册后 VM 为 Stopped 状态 | -| lastHostUuid | 设 null | 新环境无意义 | -| instanceOfferingUuid | 设 null | 新环境可能不存在 | -| imageUuid | 保留原值,目标环境不存在时置 null | 若 `dbf.findByUuid(imageUuid, ImageVO.class) == null` 则 `imageUuid = null`,并在 warnings 中记录 `"imageUuid {xxx} not found in target environment, set to null"` | -| cpuNum | 保留 | — | -| memorySize | 保留 | — | -| platform | 保留 | — | -| architecture | 保留 | — | -| hypervisorType | 保留 | — | -| type | 保留 | 保持原值(UserVm) | -| state | 硬编码 | Registering → Stopped | -| defaultL3NetworkUuid | 设 null | 网络不恢复 | -| managementNetworkUuid | 设 null | 网络不恢复 | -| accountUuid | 替换 | 当前调用者(admin) | - -### 1.2 VolumeVO - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| uuid | 保留 | 冲突时拒绝注册 | -| primaryStorageUuid | 替换 | 新主存储 UUID | -| installPath | 替换 | 路径映射(vg uuid / 挂载路径替换) | -| diskOfferingUuid | 设 null | 新环境可能不存在 | -| vmInstanceUuid | 保留 | 与注册 VM UUID 一致 | -| accountUuid | 替换 | 当前调用者 | - -### 1.3 VolumeSnapshotVO - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| uuid | 保留 | 冲突时拒绝注册 | -| primaryStorageUuid | 替换 | 新主存储 UUID | -| primaryStorageInstallPath | 替换 | 路径映射 | -| volumeUuid | 保留 | — | -| parentUuid | 保留 | 快照链关系 | - -### 1.4 SystemTagVO / ResourceConfigVO - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| id | 自增 | 数据库自动生成 | -| uuid | 重新生成 | `Platform.getUuid()` | -| resourceUuid | 保留 | 指向 VM/Volume 的 UUID 不变 | -| 其余字段 | 保留 | — | - -**重要**:元数据中的 SystemTag/ResourceConfig 已在构建时经白名单过滤(见 [Part 1a §4.1](vm-metadata-01a-数据模型与序列化.md#41-systemtagresourceconfig-构建时过滤规则)),注册时**直接恢复到 DB,无需二次过滤**。 - -### 1.5 VolumeSnapshotGroupVO(快照组) - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| uuid | 保留 | 冲突时拒绝注册 | -| name | 保留 | — | -| description | 保留 | — | -| vmInstanceUuid | 保留 | 与注册 VM UUID 一致 | -| snapshotCount | 保留 | — | -| accountUuid | 替换 | 当前调用者(admin) | -| createDate | 保留 | — | -| lastOpDate | 重新生成 | 注册时间 | - -### 1.6 VolumeSnapshotGroupRefVO(快照组引用) - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| id | 自增 | 数据库自动生成 | -| volumeSnapshotGroupUuid | 保留 | FK → VolumeSnapshotGroupVO(同事务内已创建) | -| volumeSnapshotUuid | 保留 | FK → VolumeSnapshotVO(同事务内已创建) | -| volumeUuid | 保留 | FK → VolumeVO(同事务内已创建) | -| deviceId | 保留 | 磁盘设备编号 | -| volumeType | 保留 | Root / Data | -| volumeName | 保留 | — | -| volumeSnapshotName | 保留 | — | -| volumeSnapshotInstallPath | 替换 | 路径映射 | -| snapshotDeleted | 保留 | 反映原始删除状态 | -| volumeLastAttachDate | 保留 | 原始挂载时间 | -| createDate | 保留 | — | -| lastOpDate | 重新生成 | 注册时间 | - -### 1.7 VolumeSnapshotReferenceVO(快照引用记录) - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| id | 重新生成 | auto-increment(仅作为存储主键,不作为映射锚点) | -| volumeUuid | 保留原值 | 缓存 VM 的卷 UUID(无 FK,允许悬挂) | -| volumeSnapshotUuid | 保留原值 | 缓存 VM 的快照 UUID(无 FK,允许悬挂) | -| volumeSnapshotInstallUrl | 替换 | 路径映射 | -| directSnapshotUuid | 保留原值 | 无 FK,允许悬挂 | -| directSnapshotInstallUrl | 替换 | 路径映射 | -| treeUuid | 保留 | 指向 VolumeSnapshotReferenceTreeVO.uuid | -| parentId | 直接设 null | 注册场景等效于模板缓存已删除状态,FK `ON DELETE SET NULL` 已将 parentId 置 null,无需映射回填 | -| referenceUuid | 保留原值 | — | -| referenceType | 保留 | — | -| referenceInstallUrl | 替换 | 路径映射 | -| referenceVolumeUuid | 保留 | 子 VM 自己的卷 UUID(FK → VolumeEO CASCADE) | -| createDate | 保留 | — | -| lastOpDate | 重新生成 | 注册时间 | - -### 1.8 VolumeSnapshotReferenceTreeVO(快照引用树) - -| 字段 | 处理方式 | 说明 | -|------|----------|------| -| uuid | 保留 | 冲突时跳过(幂等,多个子 VM 可能共享同一棵树) | -| rootImageUuid | 保留原值 | 无 FK | -| rootVolumeUuid | 保留原值 | 无 FK,允许悬挂 | -| rootInstallUrl | 替换 | 路径映射 | -| rootVolumeSnapshotUuid | 保留原值 | 无 FK,允许悬挂 | -| rootVolumeSnapshotTreeUuid | 保留原值 | 无 FK,允许悬挂 | -| primaryStorageUuid | 替换 | 新主存储 UUID | -| hostUuid | 按需处理 | Local 存储保留,SharedBlock 设 null | - ---- - -## 2. 跨存储数据盘处理规则 - -### 2.1 策略 - -虚拟机的所有磁盘必须位于同一主存储,否则**拒绝注册**。 - -**原因**:跨存储路径映射规则不统一,快照组跨存储引用不完整,单存储简化所有流程。 - -**拒绝返回信息(改进)**:`CROSS_STORAGE_REJECTED: VM {vmUuid} has volumes on multiple primary storages: expectedPsUuid={targetPsUuid}, actualPsUuids={ps1,ps2,...}. Registration requires all volumes on one primary storage.` - -### 2.2 SnapshotGroup 处理 - -所有磁盘在同一存储上 → SnapshotGroup 天然完整。SnapshotGroupVO 和 SnapshotGroupRefVO 在同一事务内一次性创建。 - ---- - -## 3. 注册虚拟机详细流程 - -### 3.1 状态流 - -``` -(new) → Registering → Stopped → Starting → Running - │ - └── 失败 → 回滚删除所有 VO -``` - -### 3.2 "注册 VM 未首次启动" ResourceConfig - -| 时机 | 操作 | -|------|------| -| 注册完成 | 创建 `vm.metadata.registered.not.started` ResourceConfig | -| VM 首次到达 Running 状态 | 删除该 ResourceConfig,立即触发 `markDirty` | -| 存在该 ResourceConfig 时 | 任何 `@MetadataImpact` API 的元数据更新被跳过 | - -**注册 VM + 普通 API 交互**:注册完成后、首次启动前,VM 处于 Stopped 状态且持有 `registered.not.started` ResourceConfig。 -此时若执行 `APIUpdateVmInstanceMsg`(改名/描述),`VmMetadataUpdateInterceptor.afterCompletion()` 检测到 -ResourceConfig 存在,跳过 `markDirty`。改名/描述的变更不会即时反映到元数据中。 -**这是设计意图**:注册 VM 在未启动前具有完整的原始状态元数据(Step 7 markDirtyInternal 已写入)。 -用户在 Stopped 阶段做的修改(改名等)将在 VM 首次到达 Running 时通过删除 ResourceConfig + markDirty 一次性同步。 -若业务上无法接受此延迟,可通过 `APIUpdateVmMetadataMsg` 手动触发(该 API 绕过 ResourceConfig 检查)。 - -### 3.3 完整注册步骤 - -``` -1. 前置校验 - ├── 元数据 JSON 解析 + Base64 解码 + Validator 校验 - ├── readStatus 可用性检查(见下方说明) - ├── vmCategory 类型检查 - │ ├── REGULAR / TEMPLATE → 继续注册 - │ ├── TEMPLATE_CACHE → 拒绝注册 - │ └── null(旧版元数据) → 视为 REGULAR,继续 - ├── schemaVersion 精确匹配检查(见 Part 1a §6.2) - ├── 跨存储校验:所有 Volume 归属同一目标主存储(见 §2),失败返回 expected/actual PS UUID 列表 - ├── UUID 冲突检测(VM/Volume/Snapshot/SnapshotGroup/SnapshotGroupRef/Reference/ReferenceTree) - │ ├── **批量检测策略**:所有待检测 UUID 按每批 1000 个分组查询(`SELECT uuid FROM XxxVO WHERE uuid IN (:batch)`), - │ │ 避免单次 IN 子句超过数千个参数时的 SQL 解析性能退化。对于大快照链场景(54 磁盘 × 256 快照)UUID 总数可达万级。 - │ ├── 冲突且是 Registering 遗留 → 幂等回滚后重新注册 - │ └── 冲突且是正常资源 → 拒绝 - └── installPath 替换 + 路径存在性检查(Agent 校验) - ├── Root Volume installPath 不存在 → BLOCK(拒绝注册) - └── Data Volume installPath 不存在 → WARN(允许继续) - - readStatus 可用性校验逻辑: - - ```java - // 从 metadataContent JSON 中提取 __readStatus(Read API 嵌入) - String readStatus = metadata.get("__readStatus"); - if ("CORRUPTED".equals(readStatus) || "STORAGE_CHANGE_INCOMPLETE".equals(readStatus)) { - throw new ApiMessageInterceptionException(argerr( - "METADATA_READ_STATUS_UNUSABLE: metadata readStatus is %s, " + - "cannot register. Please resolve the storage issue and re-read metadata.", - readStatus)); - } - ``` - - **背景**:Register API 接收的 `metadataContent` 通常来自 Read API。Read API 在返回时将 `__readStatus` 字段嵌入 JSON 根级别。Register 入口解析此字段,对 `CORRUPTED`(双 Slot 损坏)和 `STORAGE_CHANGE_INCOMPLETE`(存储拓扑变更未完成)状态拒绝注册。`OK`/`NEED_REPAIR`/`RECOVERED`/`DEGRADED` 状态允许继续。若 `__readStatus` 字段不存在(手动构造的 JSON),视为 OK 继续。 - - PreCheck 判定示例: - - ```java - if (volume.isRootVolume() && !pathExists(volume.getInstallPath())) { - result.add(PreCheckItem.block(INSTALL_PATH_EXIST, - "Root volume install path does not exist: " + volume.getInstallPath())); - } else if (!pathExists(volume.getInstallPath())) { - result.add(PreCheckItem.warn(INSTALL_PATH_EXIST, - "Data volume install path does not exist: " + volume.getInstallPath())); - } - ``` - -2. 创建 VmInstanceVO - ├── state = Registering - ├── 打 SystemTag: vmMetadata::registeringMnUuid::{mnUuid} - ├── 打 SystemTag: vmMetadata::registeringStartTime::{timestamp} - └── 创建 "注册VM未首次启动" ResourceConfig - -3. 还原 SystemTag / ResourceConfig - ├── 从元数据中解码(Base64 解码) - ├── 直接恢复到 DB(构建时已过滤,无需二次过滤) - └── 为每个 SystemTag/ResourceConfig 生成新 UUID(Platform.getUuid()) - -4. 创建 VolumeVO - ├── 替换 primaryStorageUuid、installPath、accountUuid - └── 还原 volume 级 SystemTag / ResourceConfig - -5. 快照还原 - ├── 每棵快照树使用 VolumeSnapshotTree.fromInventories() 构建 - │ ├── 创建 VolumeSnapshotTreeVO - │ ├── 层级遍历快照树,按顺序创建 VolumeSnapshotVO - │ └── 校验每个 parentUuid 在已创建集合中存在 - ├── 创建 VolumeSnapshotGroupVO + VolumeSnapshotGroupRefVO - ├── 创建 VolumeSnapshotReferenceVO + VolumeSnapshotReferenceTreeVO - └── 事务策略:批量 persist 每 100 条 flush + clear - - **大快照链性能说明**:极端场景下(24 磁盘 × 256 快照 = 6144 个 VolumeSnapshotVO + Group/Ref/Tree 关联记录),单事务写入量可达万级别。当前使用 `batch flush+clear per 100 rows` 缓解 JPA 一级缓存膨胀。若快照总数超过 1000,在 LongJob 进度中记录预计耗时,并在 `warnings` 中提示 `"Large snapshot chain detected ({count} snapshots), registration may take longer than usual"`。v2+ 考虑分卷分事务策略降低 Galera 复制延迟风险。 - -6. 执行变基(sblk / local / NFS) - ├── **变基前重新校验 installPath 存在性**(Agent 调用) - │ └── 若 Root Volume installPath 不存在 → 直接进入回滚路径,不执行 qemu-img 操作 - │ (Step 1 的校验与 Step 6 之间可能经过数分钟 VO 创建,存储侧可能发生变化) - ├── 幂等:先 qemu-img info 检查当前 backing file - │ ├── 已指向目标路径 → 跳过 - │ ├── 指向旧路径 → 执行 qemu-img rebase -u - │ └── 指向异常路径 → 报错 - └── 变基失败 → 整个注册回滚 - -7. 注册成功 - ├── 更新 VmInstanceVO.state = Stopped + 删除 registeringMnUuid tag(同一事务内,保证原子性) - ├── 事务提交后立即调用 `markDirtyInternal(vmUuid, true)` ← 绕过 ResourceConfig 检查,确保首次元数据写入 - │ **markDirtyInternal 机制说明**:`markDirtyInternal` 并非一个独立方法,而是直接调用 `markDirty()` 的内部路径。 - │ 拑制发生在 `VmMetadataUpdateInterceptor.afterCompletion()` 中——当检测到 - │ `registered.not.started` ResourceConfig 存在时,拦截器 skip `markDirty` 调用。 - │ 注册 Step 7 的 `markDirtyInternal` 不经过拦截器,而是从服务内部直接调用 - │ `VmMetadataDirtyMarker.markDirty(vmUuid, storageStructureChange=true)`,因此不受 ResourceConfig 拑制。 - ├── 异步触发 `APICheckVmInstanceMetadataConsistencyMsg(autoRepair=true)` 做注册后一致性复核;若发现不一致,在 Event.warnings 中记录差异项 - └── 返回结果(含 warnings) - - **注意**:`registered.not.started` ResourceConfig **不在此步删除**。该 Config 的完整生命周期见 §3.2:创建于 Step 2 → 抑制 Stopped 阶段的 `@MetadataImpact` API 触发 markDirty → VM 首次到达 Running 时删除并触发 markDirty。 -``` - -### 3.4 sblk 变基详细流程 - -``` -原存储: sblkA (vg_uuid = "123xxx") -新存储: sblkB (vg_uuid = "456xxx") - -步骤: - 1. 替换 VO 中 vg uuid: 123xxx → 456xxx(前缀锚定替换) - 2. 校验替换后 installPath 在存储上存在 - 3. 创建所有 VO - 4. 变基: qemu-img rebase -u -b <新backing路径> <当前LV路径> -``` - -**路径替换安全机制**:使用 `String.startsWith(oldPrefix)` 检查后字符串拼接,路径格式通过正则预校验。 - -**分隔符边界保护**:前缀锚定时要求 `oldPrefix` 以路径分隔符结束(如 `/oldVg/`),并验证替换点满足边界条件,避免将 `oldVg` 误命中为 `oldVg2` 或其他子串。 - -### 3.5 Registering 状态 VM 的可见性 - -- `Registering` 状态 VM **仅 admin 可见** -- 普通用户 `QueryVmInstance` 自动过滤 -- admin 可查询但变更操作被拦截器拒绝 - -**Registering 状态 API 拦截实现位置**:在 `VmInstanceBase` 中统一处理,而非在每个 API handler 中单独检查。`VmInstanceBase.handleMessage()` 入口处增加 `state == Registering` 检查:除 `QueryVmInstanceMsg` 和内部注册消息外,所有其他消息均返回 `VM_IN_REGISTERING_STATE` 错误码。这避免了在新增 API 时遗漏添加 Registering 状态拦截的风险。 - ---- - -## 4. 注册事务回滚 - -### 4.1 回滚触发条件 - -MN 启动时扫描 `state=Registering` 的 VM,检查 `registeringMnUuid` SystemTag: - -| tag 中 mnUuid | 条件 | 行为 | -|---------------|------|------| -| = 当前 MN UUID | — | 回滚 | -| ≠ 当前 MN UUID | 该 MN **不在线** | 回滚 | -| ≠ 当前 MN UUID | 该 MN **在线** | 跳过 | - -**触发时机**:`managementNodeReady()` / `ManagementNodeLeftEvent` 回调。 - -### 4.2 回滚操作 - -注册流程中通过 `Set createdVoUuids` 跟踪每步实际创建的 VO UUID。回滚时仅删除此集合中的记录,而非尝试删除“应该存在”的所有元数据对象——避免崩溃发生在中间步骤时删除未创建的 VO 引发的无效查询或 FK 异常。 - -按以下顺序删除当前注册创建的所有 VO: - -1. VolumeSnapshotReferenceTreeVO(外层对象;**删除前检查依赖**:若该 TreeVO 下仍有其他 VM 的 ReferenceVO 行(多个子 VM 共享同一棵树),则保留 TreeVO 不删除,仅删除当前 VM 的 ReferenceVO 行) -2. VolumeSnapshotGroupRefVO / VolumeSnapshotGroupVO -3. VolumeSnapshotVO -4. VolumeVO(含 SystemTag / ResourceConfig) -5. VmInstanceVO(含 SystemTag / ResourceConfig) - -回滚顺序采用“由外到内”原则,优先删除聚合根对象,利用数据库级联减少中途失败导致的残留。 - -**回滚后防御性清理 SQL**(幂等,可重复执行): - -```sql -DELETE t FROM VolumeSnapshotReferenceTreeVO t -LEFT JOIN VolumeSnapshotReferenceVO r ON r.treeUuid = t.uuid -WHERE r.id IS NULL; -``` - -**存储数据不删除**:存储上的数据是用户迁移的,不因注册失败而删除。 - -### 4.3 幂等与可重入 - -每步 `DELETE` 天然幂等。回滚中途再次崩溃 → 下次启动重新检测到 `Registering` → 继续回滚。 - -### 4.4 LongJob 超时与取消回滚 - -- 注册 LongJob 超时时,`cancel()` 必须调用 `rollbackRegistration(vmUuid)`,复用与失败路径相同的回滚逻辑。 -- 超时后的中间态 VM 必须保留在 `Registering`,使后续 UUID 冲突检测可识别为“Registering 遗留”并触发幂等回滚重试。 -- **LongJob 超时时长来源**:从 `APIRegisterVmInstanceFromMetadataMsg` 的 API timeout 配置推导(默认 30 分钟),而非硬编码。ChainTask 超时 = API timeout + 5 分钟余量。`registeringStartTime` 过期判定同样基于此配置值 + 5 分钟,而非硬编码 35 分钟。这允许运维通过调整 API timeout 统一控制注册超时行为。 - ---- - -## 5. 注册场景问题分析 - -### 5.1 UUID 冲突 - -注册前批量查询所有涉及的 UUID,任一冲突立即拒绝。检测到冲突时判断是否为 Registering 遗留 → 是 → 回滚后重新注册。 - -### 5.2 installPath 映射 - -路径映射采用**自动推导**,无需用户手动提供: - -| 存储类型 | 旧路径标识符来源 | 新路径标识符来源 | -|----------|----------------|----------------| -| sblk | 从元数据 VolumeVO.installPath 提取旧 VG UUID | 目标主存储 VG UUID | -| local/NFS | 从元数据 VolumeVO.installPath 提取旧挂载路径 | 目标主存储 mountPath | - -**文件不移动**:注册流程中不移动文件。账户替换只在 DB 层面。 - -### 5.3 元数据损坏/不完整 - -JSON 解析 / Base64 解码 / 校验器任一步骤失败 → 拒绝注册。sblk 双 Slot 容错机制详见 [Part 4d](vm-metadata-04d-sblk读取与恢复.md)。 - -### 5.4 快照链变基的幂等性 - -| 当前 backing file | 行为 | -|-------------------|------| -| 已指向目标路径 | 跳过 | -| 指向旧路径 | 执行变基 | -| 指向其他路径 | 报错 | - -### 5.5 部分快照树失败 - -原子性以 **VM 为粒度**:任一快照树失败 → 整个注册回滚。 - -### 5.6 并发操作 - -- Registering 状态 VM 只允许查询 -- ChainTask `syncSignature = vm-register-{vmUuid}` -- ChainTask 超时:`timeout = API timeout + 5 分钟余量`(见 §4.4,从 API 配置推导) -- **DB 主键是跨 MN 互斥的最终保证**:`INSERT VmInstanceVO(uuid=xxx, state=Registering)` 的主键重复即拦截并发注册。即使两个 MN 同时对同一 VM 发起注册,先提交的事务成功,后提交的因主键冲突失败。 - -**注册部分创建窗口分析**:Step 2 创建 VmInstanceVO 与 Step 5 创建快照之间可能经过数分钟(大快照链)。在此窗口内 VM 处于 Registering 状态,只允许查询操作(见此节第一条),且 Registering 状态的 VM 不会被 Poller 处理(无 dirty 行,因 markDirty 发生在 Step 7)。因此部分创建状态对外部操作不可见、不会被操作,安全。 - -### 5.7 无网卡 VM 的启动行为 - -注册后 VM 无网卡是允许的状态。推荐流程:先加网卡(`AttachVmNicToVm`),再启动。 - -#### 5.7.1 为什么不恢复网络 - -注册不恢复网络(NIC、L3 绑定、IP 分配、安全组),原因如下: - -1. **L3 网络拓扑不可迁移**:源环境的 L3 网络 UUID、VLAN ID、CIDR 在目标环境中不存在或不相同 -2. **IP 地址冲突风险**:源环境的 IP 可能已在目标环境中被分配给其他 VM -3. **安全组/VPC 规则依赖环境**:防火墙规则引用的 SecurityGroup UUID、VPC UUID 均为环境专属 - -#### 5.7.2 手动网络恢复步骤 - -``` -1. 查看注册 warnings 中输出的原始 NIC 信息: - "Original NIC config: {nicUuid, l3NetworkUuid, ip, mac, deviceId}" - -2. 在目标环境中选择对应的 L3 网络: - - 若已有匹配网段的 L3 → 直接使用 - - 若无 → 先创建 L3Network + IP Range - -3. 挂载网卡: - APIAttachVmNicToVmMsg(vmInstanceUuid, l3NetworkUuid) - → 系统自动分配 IP + MAC - -4. (可选)恢复安全组绑定: - APIAddVmNicToSecurityGroupMsg(securityGroupUuid, vmNicUuids) - -5. 启动 VM: - APIStartVmInstanceMsg(uuid) -``` - -**注意**:注册时会在 `warnings` 中输出所有原始 NIC 配置信息(`l3NetworkUuid`、`ip`、`mac`、`deviceId`),供运维参考。目标环境中 MAC 地址会重新生成,不会与源环境冲突。 - -### 5.8 链式克隆虚拟机注册 - -#### 核心原则 - -注册后的子 VM 等效于**模板和缓存已被删除**的状态。不恢复模板、缓存及其快照的 DB 记录。 - -#### 注册流程差异 - -| 项目 | 普通 VM | 链式克隆子 VM | -|------|---------|--------------| -| VmInstanceVO | 创建 | 创建 | -| VolumeVO | 创建 | 创建 | -| VolumeSnapshotVO | 恢复子 VM 自己的 | 恢复子 VM 自己的 | -| VolumeSnapshotReferenceTreeVO | 不涉及 | check existence → skip or create | -| VolumeSnapshotReferenceVO | 不涉及 | 直接插入,parentId 统一置 null | - -#### parentId 处理策略 - -注册场景等效于**模板缓存已被删除**的状态。缓存 VM 删除时,`VolumeSnapshotReferenceVO.parentId` 的 FK(`ON DELETE SET NULL`,自引用)已将所有子引用的 parentId 置为 null。因此注册时无需映射回填,直接全量插入 `parentId = null` 即可。 - -``` -1. 插入全部 VolumeSnapshotReferenceVO,parentId 统一置 null -2. 无需第二阶段回填 -``` - -**简化理由**:注册的子 VM 不可能依赖缓存 VM 的其他 Reference 行(缓存 VM 未被注册),因此 parentId 引用链在新环境中天然为空。 - -#### TreeVO 幂等性 - -多个子 VM 可能共享同一棵树。使用 UUID 做存在性检查: - -```java -if (!Q.New(VolumeSnapshotReferenceTreeVO.class) - .eq(VolumeSnapshotReferenceTreeVO_.uuid, treeVO.getUuid()) - .isExists()) { - dbf.persist(treeVO); -} -``` - ---- - -## 6. 可观测性 - -### 6.1 运维告警 - -新增报警器:**更新虚拟机元数据失败**。触发条件:达到最大重试次数仍失败。 - -### 6.2 一致性检查 - -`APICheckVmInstanceMetadataConsistencyMsg`:从 DB 构建元数据 → 从存储读取 → 结构化比较。排除 `lastOpDate`、`id`、`managementNodeUuid` 字段。 - -API 详细定义见 [Part 5 §5](vm-metadata-05-API设计.md#5-检查虚拟机元数据一致性)。 - -### 6.3 注册预检查 - -`APIPreCheckVmMetadataRegistrationMsg`:检查 UUID 冲突、PS 可达性、版本兼容等。 - -API 详细定义见 [Part 5 §6.2](vm-metadata-05-API设计.md#62-注册预检查)。 - -### 6.4 手动触发元数据更新 - -`APIUpdateVmMetadataMsg`:指定 vmUuid,手动触发一次全量元数据更新。 - -API 详细定义见 [Part 5 §6.1](vm-metadata-05-API设计.md#61-手动触发元数据更新)。 - ---- - -## 7. 设计决策汇总 - -| 问题域 | 决策 | 理由 | -|--------|------|------| -| UUID 冲突 | 前置全量检查 + Registering 幂等回滚 | 防重复注册 | -| MN 崩溃 | SystemTag 标记 + 启动扫描 | 防中间状态泄漏 | -| 版本不匹配 | 默认拒绝 + `forceVersionMismatch` 允许强制注册 | 兼顾安全性和灵活性。**`forceVersionMismatch=true` 时字段映射策略**:(1) 目标版本新增但源版本缺失的字段 → 使用 Java 默认值(null/0/false),Gson 反序列化自动处理;(2) 源版本存在但目标版本已移除的字段 → 忽略(Gson 默认丢弃未知字段);(3) 字段类型变更 → Gson 抛异常,注册失败并在 warnings 中列出被忽略/使用默认值的字段名列表 | -| 路径映射 | 自动推导 + 前缀锚定替换 + 文件存在性检查 | 简单可靠 | -| 跨存储 | 拒绝注册 | 消除复杂性 | -| 跨存储错误信息 | 返回 expected/actual PS UUID 明细 | 运维可直接定位冲突卷 | -| SystemTag 过滤 | 构建时白名单过滤,注册时直接恢复 | 无需二次过滤 | -| 模板 VM | 注册为普通 VM | 纯标记表无业务字段。**注意**:注册后不保留模板身份,若需要恢复模板功能需手动转换(`APIChangeVmInstanceToTemplateMsg`)。注册时 warnings 中记录 `"VM {uuid} was a template VM, registered as regular VM"`。**降级后丢失的能力**:(1) 不能从此 VM 创建链式克隆子 VM;(2) 不能作为模板发布到镜像市场;(3) 不能被其他用户用作创建 VM 的模板。**恢复方式**:调用 `APIChangeVmInstanceToTemplateMsg(vmInstanceUuid)` 将 VM 转回模板 | -| 缓存 VM | 写入元数据但拒绝注册 | 运行态产物,新环境自动创建 | -| 链式克隆子 VM | 仅恢复 Reference 表 | 等效于模板已删除状态 | -| Reference parentId 映射 | 直接置 null(等效模板缓存已删除) | 无需映射回填,简化注册流程 | -| 存储数据 | 注册回滚不删除 | 数据由用户迁移 | -| 变基幂等 | `qemu-img info` 预检查 | 支持安全重试 | -| 文件移动 | 不移动,仅 DB 替换 accountUuid | 避免大文件移动风险 | -| 注册超时 | API timeout + 5 分钟余量 ChainTask + cancel 回滚 | 从 API 配置推导,避免硬编码;防止 LongJob 超时残留 | -| Root 路径不存在 | `INSTALL_PATH_EXIST` 视为 BLOCK | Root 缺失不可启动 | -| 注册后校验 | 触发 ConsistencyCheck | 及早暴露存储/DB 偏差 | - ---- - -## 8. 运维指南:注册失败后的清理 - -### 8.1 自动清理 - -MN 启动时自动扫描 `state=Registering` 的 VM 并回滚(见 §4.1)。正常情况下无需手动干预。 - -### 8.2 手动清理场景 - -当自动回滚未成功时(极端场景),运维可按以下顺序手动清理: - -```sql --- 1. 查找残留的 Registering VM -SELECT uuid, name FROM VmInstanceVO WHERE state = 'Registering'; - --- 2. 按依赖顺序删除(先子后父) --- 步骤同 §4.2 回滚操作:ReferenceTree→Reference→GroupRef→Group→Snapshot→Volume→VM -DELETE FROM VolumeSnapshotReferenceVO WHERE referenceVolumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); -DELETE FROM VolumeSnapshotGroupRefVO WHERE volumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); -DELETE FROM VolumeSnapshotVO WHERE volumeUuid IN (SELECT uuid FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'); -DELETE FROM VolumeVO WHERE vmInstanceUuid = '{vmUuid}'; -DELETE FROM VmInstanceVO WHERE uuid = '{vmUuid}'; -``` - -**重要**:存储上的数据不删除。存储数据由用户迁移而来,不因注册失败而清理。 - ---- - -## 9. 约束与不変量 - -| 约束 ID | 约束描述 | 违反后果 | 检查点 | -|---------|----------|----------|--------| -| C-03-1 | `VolumeSnapshotReferenceVO.parentId` 注册时统一置 null(等效模板缓存已删除状态),不做映射回填 | — | 注册步骤 §3.3-5 与链式克隆 §5.8 | -| C-03-2 | 跨存储(同 VM 卷分布多个 PS)必须拒绝注册,并返回 expected/actual PS UUID 明细 | 错误路径映射、快照组不完整 | 前置校验 §2.1 / §3.3-1 | -| C-03-3 | installPath 前缀替换必须满足分隔符边界(`/oldPrefix/`) | 子串误替换导致路径污染 | 路径映射 §3.4 / §5.2 | -| C-03-4 | 回滚删除顺序必须“由外到内”,并执行空树清理 SQL | Tree/Reference 残留与数据泄漏 | 回滚 §4.2 | -| C-03-5 | 注册 ChainTask 超时从 API timeout 配置推导(+ 5 分钟余量);LongJob cancel 必须触发 `rollbackRegistration` | Registering 残留、后续冲突误判 | 并发与超时 §4.4 / §5.6 | -| C-03-6 | Root Volume `INSTALL_PATH_EXIST` 缺失必须 BLOCK;Data Volume 可 WARN | 注册成功但 VM 无法启动 | 前置校验 §3.3-1 | -| C-03-7 | 注册成功后必须触发一次 ConsistencyCheck | 存储与 DB 偏差延迟暴露 | 成功收敛 §3.3-7 / 可观测性 §6.2 | -| C-03-8 | PreCheck 与 Register **必须共享同一校验方法**(如 `validateRegistration()`),PreCheck = validate only,Register = validate + execute。新增校验项时只需修改一处 | PreCheck 通过但 Register 失败(或反之),用户体验矛盾 | 前置校验 §3.3-1 / PreCheck §6.3 / [Part 5 §6.2](vm-metadata-05-API设计.md#62-注册预检查) | diff --git "a/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" "b/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" deleted file mode 100644 index 1d49ee78275..00000000000 --- "a/docs/design/vm-metadata-04a-sblk\345\255\230\345\202\250\345\215\217\350\256\256\346\246\202\350\277\260.md" +++ /dev/null @@ -1,248 +0,0 @@ -# VM 元数据 — sblk 二进制存储协议概述 - -## 1. 术语表 - -| 术语 | 定义 | -|------|------| -| sblk | ZStack 共享块存储(Shared Block Storage),基于 LVM 的裸块设备 | -| LV | LVM Logical Volume,元数据持久化的最小存储单元 | -| Header Block | LV 头部 4KB 区域,存放控制信息与 VM 摘要 | -| Slot | 数据槽,存放完整的 payload(元数据 DTO JSON) | -| Active Slot | Header.ActiveSlot 指向的当前有效 Slot | -| Inactive Slot | 未被 ActiveSlot 指向的另一个 Slot,写入目标 | -| PendingOp | Header 中的操作意图标记,0=空闲 / 1=CONFIG_UPDATE / 2=STORAGE_CHANGE | -| WriteSequence | 单调递增写计数器,用于判读 Slot 数据是否属于最新一次写入 | -| ControlChecksum | SHA-256(Header[0:64]),覆盖崩溃恢复关键字段 | -| SummaryChecksum | SHA-256(Header[96:896]),覆盖 VM 摘要字段 | -| O_DIRECT | Linux 直接 I/O 标志,绕过 page cache | -| ALIGNMENT | 4096 字节,O_DIRECT I/O 对齐粒度 | -| op_type | 写入操作类型,由控制面 `@MetadataImpact` 注解决定 | -| Full-refresh | 从管理面数据库重建完整元数据并全量写入 LV | - ---- - -## 2. 适用范围 - -本文档仅覆盖 **sblk(共享块存储)** 场景下的二进制存储协议。 - -| 场景 | 存储协议 | 并发控制 | 崩溃安全机制 | -|------|---------|---------|-------------| -| **sblk** | 本文档:二进制 Header + A/B Dual Slot | 管理面四层串行化 (Part 2 §3.1) | A/B Dual Slot + PendingOp | -| local/NFS | JSON 明文 + tmp + fsync + rename | flock (defense-in-depth) | 原子 rename | - -> local/NFS 场景不使用 op_type、PendingOp 等概念(JSON atomic rename 无中间状态)。后文所有设计均仅针对 sblk。 - ---- - -## 3. 背景与动机 - -ZStack 共享块存储(sblk)场景下,VM 元数据需要持久化到 LVM Logical Volume 上。多个管理节点可能通过共享块设备并发访问同一 LV。 - -核心挑战: - -- **无文件系统**:LV 是裸块设备,无法使用常规文件 I/O -- **共享访问**:多节点通过 O_DIRECT 绕过 page cache 直接读写 -- **崩溃安全**:任意时刻断电或进程崩溃后,数据必须可恢复 -- **空间受限**:LV 初始 4MB,最大 64MB,需高效利用 - -### 3.1 灾备接管 — A/B 双 Slot 的核心驱动力 - -> **脑裂不在设计范围内**:如果两个平台同时对同一 LV 执行写入操作,A/B Slot 不提供保护。预留 Header Reserved 区 8B 用于存储 `platformId`,未来可用于检测跨平台写入冲突。 - -除常规读写外,协议必须支持**跨平台灾备接管**场景: - -``` -环境: - sanA / sanB — 两套拥有相同 LUN(数量和大小)的 SAN 存储 - zsvA(原平台)/ zsvB(目标平台)— 两套独立的 ZSV 管理平台 - -操作流程: - 1. zsvA 将 sanA 添加为 sblk 存储,在上面创建 VM 并正常读写 - 2. zsvB 将 sanB 添加为存储目标(iSCSI server),但不注册为 sblk 存储 - 3. 存储侧配置 sanA → sanB 的 LUN 级数据复制(块级,平台不感知) - 4. zsvA 的 sanA 发生故障 - 5. zsvB 使用 sanB 注册 sblk 存储,通过扫描 LV 上的元数据恢复 VM -``` - -此场景下 LV 元数据的角色发生本质转变: - -| | 正常运行 | 灾备接管 | -|---|---------|---------| -| 元数据权威来源 | 管理面 DB | **LV 上的元数据** | -| LV 元数据角色 | DB 的副本/缓存 | **唯一的 VM 恢复来源** | -| 管理面 DB 可用? | (Y) zsvA DB 可用 | (N) zsvA 故障,zsvB DB 无此 VM | -| Full-refresh 可行? | (Y) 从 DB 重建 | (N) **无 DB 数据可重建** | - -**核心问题**:存储侧复制是**块级别快照**,可能捕获到 LV 正在写入的中间状态。 - -单区覆盖写在此场景下的风险: - -``` -zsvA 正在写入: 已写入部分新数据,旧数据已被覆盖 -此刻 sanA → sanB 块级复制发生 -sanB 上的 LV: payload 损坏 + 旧数据不可恢复 + zsvB 无 DB → VM 不可恢复 (N) -``` - -A/B 双 Slot 的保证: - -``` -zsvA 正在写入: Phase 2 写入 Inactive Slot,Active Slot 未被触碰 -此刻 sanA → sanB 块级复制发生 -sanB 上的 LV: Active Slot 完整有效 → zsvB 读到旧元数据 → VM 可注册 (Y) -``` - -> **结论**:A/B Dual Slot 是能保证任意复制时刻都有可读数据的最简方案。协议复杂度是为灾备可靠性买单。 - ---- - -## 4. 设计目标 - -| 目标 | 要求 | -|------|------| -| 原子性 | 任意崩溃点数据不损坏,已提交数据不丢失 | -| 自描述 | Slot 自带位置信息,Header 损坏时仍可恢复 | -| 高效 I/O | O_DIRECT + O_SYNC,对齐到 4KB 页边界 | -| 简单可靠 | 纯二进制定长字段,无 JSON 解析开销 | -| 可观测 | hexdump 直接可读,状态可诊断 | -| 前向兼容 | HeaderVersion 管布局演进,SchemaVersion 管 payload 演进 | - ---- - -## 5. 整体架构 - -LV 初始预分配 4MB 空间(虚拟机在正常使用场景下,元数据一般只有几十 KB)。直接以 Raw Data 存储 JSON 元数据,不格式化文件系统。采用 **预分配固定大小 LV + Raw Data 存储 + A/B 分区原子写** 方案,规避频繁创建/删除 LV 的性能问题。 - -``` -LV Layout (e.g. 4MB) -┌──────────────┬────────────────────┬────────────────────┐ -│ Header Block │ Slot A │ Slot B │ -│ 4KB │ ~2MB │ ~2MB │ -│ (4096B) │ │ │ -└──────────────┴────────────────────┴────────────────────┘ -offset: 0 4096 4096 + SlotACapacity - -空间计算公式(4KB 对齐): -available = LV_SIZE - 4096 -slot_capacity = floor(available / 2 / 4096) * 4096 - -示例(4MB LV): -available = 4194304 - 4096 = 4190208 -slot_capacity = floor(4190208 / 2 / 4096) * 4096 = 2093056 - -Header: [0, 4096) -Slot A: [4096, 2097152) 约 2043 KB -Slot B: [2097152, 4190208) 约 2043 KB -Tail: [4190208, 4194304) 约 4 KB (未使用) -``` - -- **Header Block (4096B)**:控制信息 + VM 摘要信息,O_DIRECT 单次写入 -- **Slot A / Slot B**:双槽交替写入,A/B 切换实现原子更新 - -### 5.1 已知 LV 大小集合(KNOWN_LV_SIZES) - -为了支撑扩容后恢复与多布局回退,协议约束可识别的历史 LV 大小集合为: - -```python -KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] -``` - -对应字节值(9 个固定值): - -```python -KNOWN_LV_SIZES = [ - 4 * 1024 * 1024, - 6 * 1024 * 1024, - 8 * 1024 * 1024, - 12 * 1024 * 1024, - 16 * 1024 * 1024, - 24 * 1024 * 1024, - 32 * 1024 * 1024, - 48 * 1024 * 1024, - 64 * 1024 * 1024, -] -``` - -该集合与 Part 4e 的阶梯扩容规则保持一致,供 Part 4d 的 multi-layout 恢复穷举使用。 - -### 5.2 A/B Dual Slot 工作模式 - -``` -正常状态 (ActiveSlot=A): - 读取 → Slot A (当前有效数据) - -写入时: - Phase 1 → 标记 intent 到 Header - Phase 2 → 写新数据到 Slot B (inactive) - Phase 3 → 切换 ActiveSlot 到 B + 清除 intent - -下次写入: - Phase 1 → 标记 intent - Phase 2 → 写新数据到 Slot A (此时 inactive) - Phase 3 → 切换 ActiveSlot 到 A -``` - -交替写入确保:**任意崩溃点至少有一个 Slot 包含完整有效数据**。 - -> **PendingOp 恢复**:若写入在 Phase 1~Phase 2 之间崩溃(Header.PendingOp≠0),读取时触发 `repair_pending_op` 恢复流程,通过双布局尝试(old-layout → new-layout)定位 Target Slot 并完成 Phase 3。完整恢复逻辑详见 [Part 4d §4](vm-metadata-04d-sblk读取与恢复.md)。 - -### 5.3 版本管理 - -两个独立的版本号,职责分离: - -| 版本号 | 位置 | 含义 | 何时递增 | -|--------|------|------|---------| -| HeaderVersion | Header Block | 二进制布局版本(字段偏移、大小、Checksum 算法) | 增删 Header/Slot 字段时 | -| SchemaVersion | Header Block | Payload JSON 业务 schema 版本 | Payload 中 JSON 字段增减时 | - -读取策略: -- `HeaderVersion > MAX_KNOWN` → 拒绝解析,提示升级软件 -- `SchemaVersion > MAX_KNOWN` → 可读出 payload,但提示部分字段可能无法识别 - -### 5.4 崩溃安全模型 - -> 本节为全文档共享的崩溃安全设计原则,Part 4c/4d/4e 中引用而不重复展开。 - -**核心声明**: -- **协议不依赖单次 4KB I/O 的原子性**。尽管当前主流 SSD/HDD 在扩区层面提供 512B~4KB 原子写入,但协议不将其作为安全假设。崩溃安全完全依赖 A/B Dual Slot 机制。 -- **读取路径不依赖 LV 大小计算 Slot 位置**。Slot 定位信息从 Header 中显式读取(`SlotAOffset`/`SlotBOffset`),而非从 `lv_size` 计算。这保证了 LV 扩容后,旧 Header 中的偏移仍然有效。 - -**部分写入分析**(Header 4KB 写入中途崩溃): - -| 崩溃时刻 | 已写入字段 | 未写入字段 | 影响 | -|----------|----------|----------|------| -| Phase 1 写 Header 中途 | PendingOp 可能已写 | ActiveSlot 未变 | Active Slot 完整,数据安全 | -| Phase 3 写 Header 中途 | ActiveSlot 可能已切换 | ControlChecksum 未更新 | 校验失败 → 进入恢复流程 → 从 Slot 自描述恢复 | -| Phase 3 完全成功 | 所有字段已写 | — | 正常 | - -> 即使单次 4KB 写入不原子,最坏情况是 Header 损坏 + ControlChecksum 不匹配,此时恢复流程(Part 4d §3)通过 Slot 自描述信息 + Checksum 找到有效数据。 - -**崩溃安全机制摘要**: - -1. Header 为 4KB,通过单次 O_DIRECT + O_SYNC 写入 -2. Phase 1 不切换 ActiveSlot → 崩溃后 Active Slot 定位信息完好 -3. Phase 2 写入 Inactive Slot → Active Slot 数据不受影响 -4. Phase 3 才切换 ActiveSlot + 更新布局 → 提交语义 -5. 即使 Header 写入中途崩溃(部分字段更新),恢复流程通过 Slot 自描述 + Checksum 找到有效数据 - -**VM 摘要区降级**:摘要区 [96, 928) 仅用于扫描优化,写入中途崩溃 → SummaryChecksum 校验失败 → 降级读 Slot,不影响正确性。 - -### 5.5 Python 2 兼容性 - -当前 Agent 环境为 Python 2.7(与 ZStack KVM Agent 一致),代码按 Python 2 编写: - -- `struct.pack/unpack` 处理大端序二进制 -- `ctypes` 分配对齐内存缓冲区(O_DIRECT 要求) -- `buffer()` 实现零拷贝写入 -- `hashlib.sha256` (Python 2.7+ 内置) -- Python 3 迁移随 Agent 整体迁移计划进行,不单独迁移 - ---- - -## 6. 文档导航 - -| 子文档 | 内容 | 典型读者 | -|--------|------|---------| -| [Part 4b — 二进制布局](vm-metadata-04b-sblk二进制布局.md) | Header Block 与 Slot 的字段定义、设计理由、hexdump 示例 | 协议实现者 | -| [Part 4c — 写入流程](vm-metadata-04c-sblk写入流程.md) | 三阶段原子写入、崩溃场景分析、状态转换图 | 协议实现者、CR 审查者 | -| [Part 4d — 读取与恢复](vm-metadata-04d-sblk读取与恢复.md) | 读取分支、Header 损坏恢复、`DEGRADED` 降级读取、multi-layout 修复与 Repair/Full-Refresh | 协议实现者、运维 | -| [Part 4e — 运维与 I/O 细节](vm-metadata-04e-sblk运维与IO.md) | LV 管理、扩容、初始化、健康检查、AlignedBuffer 代码 | Agent 开发者、运维 | diff --git "a/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" "b/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" deleted file mode 100644 index 4d44207a168..00000000000 --- "a/docs/design/vm-metadata-04b-sblk\344\272\214\350\277\233\345\210\266\345\270\203\345\261\200.md" +++ /dev/null @@ -1,289 +0,0 @@ -# VM 元数据 — sblk 二进制布局 - -## 1. Header Block (4096 Bytes) - -Header 大小等于 O_DIRECT ALIGNMENT(4KB),单次对齐 I/O 即可完成读写。内部分为三个区域: - -| 区域 | 偏移范围 | 用途 | -|------|---------|------| -| 控制区 | [0, 96) | 崩溃恢复关键字段 + ControlChecksum | -| VM 摘要区 | [96, 928) | 扫描优化字段 + SummaryChecksum | -| 预留区 | [928, 4096) | 未来扩展,零填充 | - -### 1.1 字段定义 - -``` -═══════════════════════════════════════════════════════════════════════════════ -控制区 [0, 96) — 崩溃恢复关键字段 + ControlChecksum -═══════════════════════════════════════════════════════════════════════════════ -Offset Size Field Type Description -────── ───── ──────────────── ────────── ────────────────────────────────────────── -0 4B Magic uint32 BE 固定 0x5A534D54 ("ZSMT") -4 2B HeaderVersion uint16 BE 二进制格式版本号,当前 = 1 -6 1B ActiveSlot uint8 0 = Slot A,1 = Slot B -7 1B PendingOp uint8 0 = 无,1 = config_update,2 = storage_change -8 8B WriteSequence uint64 BE 单调递增写计数器 -16 8B SlotAOffset uint64 BE Slot A 在 LV 中的字节偏移 -24 8B SlotACapacity uint64 BE Slot A 容量(字节) -32 8B SlotBOffset uint64 BE Slot B 在 LV 中的字节偏移 -40 8B SlotBCapacity uint64 BE Slot B 容量(字节) -48 8B LastUpdateTime uint64 BE 最后成功写入的 epoch 毫秒 -56 8B SchemaVersion uint64 BE Payload JSON schema 版本(扩容后 20 bit/段) -────── -64B (以上为 ControlChecksum 覆盖范围) -────── -64 32B ControlChecksum raw bytes SHA-256(bytes[0:64]) - -═══════════════════════════════════════════════════════════════════════════════ -VM 摘要区 [96, 928) — 扫描优化字段 + SummaryChecksum -═══════════════════════════════════════════════════════════════════════════════ -96 1B VmCategory uint8 VM 类别(0=REGULAR, 1=TEMPLATE, 2=TEMPLATE_CACHE) -97 32B VmUuid ASCII VM UUID hex 字符串(32 字符,无连字符) -129 2B VmNameLen uint16 BE VmName 实际字节长度(0 表示未设置) -131 765B VmName UTF-8 VM 名称(varchar(255)×utf8,最大 765 字节) -────── -896B (以上 VM 摘要字段,[96:896) 为 SummaryChecksum 覆盖范围) -────── -896 32B SummaryChecksum raw bytes SHA-256(bytes[96:896]) - -═══════════════════════════════════════════════════════════════════════════════ -预留区 [928, 4096) — 未来扩展 -═══════════════════════════════════════════════════════════════════════════════ -928 3168B Reserved zero 零填充,未来扩展使用 - -══════ -Total: 4096B -``` - -### 1.2 字段设计理由 - -**Magic (4B, offset 0)** -- `0x5A534D54` = ASCII "ZSMT" (ZStack Metadata) -- hexdump 一眼可辨识 -- brute-force 恢复时每个 4KB 对齐位置只需读前 4 字节判断 - -**HeaderVersion (2B, offset 4)** -- 二进制布局版本,只在 Header/Slot 结构变更时递增 -- uint16 足够(不可能有 65535 次布局变更) -- 与 SchemaVersion 职责分离:HeaderVersion 管"怎么读",SchemaVersion 管"读出的 JSON 怎么解释" - -**ActiveSlot (1B, offset 6) + PendingOp (1B, offset 7)** -- 各 1B 足够(`PendingOp` 当前已定义取值 0~2) -- 不用 bit flags:语义清晰,调试简单 -- 紧凑排列,在同一个 8B 对齐块内 -- 前向兼容:预留值域 **3~255**。读取端遇到未知值时按 `STORAGE_CHANGE` 语义处理(保守路径),不做 `CONFIG_UPDATE` 快速清理,避免误判导致数据丢失 - -**PendingOp 前向兼容约束(Q4-6)** - -| 取值 | 语义 | 处理策略 | -|------|------|----------| -| 0 | NONE | 正常读取流程 | -| 1 | CONFIG_UPDATE | 可按配置变更修复策略清理 PendingOp | -| 2 | STORAGE_CHANGE | 走存储变更保守修复路径 | -| 3~255(未知) | 未来扩展保留值 | **按 STORAGE_CHANGE 处理**(保守回退) | - -> 设计理由:未知 PendingOp 若误按 CONFIG_UPDATE 清理,可能在扩容/布局切换未完成场景提前丢弃恢复机会;按 STORAGE_CHANGE 保守处理最多增加一次 full-refresh,不会扩大数据风险。 - -**WriteSequence (8B, offset 8)** -- uint64,理论上限 ~1.8×10¹⁹ -- 以 1000 次/秒计算,约 5.84 亿年溢出 -- 自然对齐在 offset 8 - -**SlotAOffset / SlotBOffset (各 8B, offset 16/32)** -- **显式存储**,不再通过 `SlotBOffset = ALIGNMENT + SlotACapacity` 间接计算 -- 消除了 SlotACapacity 修改导致 SlotB 定位错误的连锁风险 -- 恢复时直接从 raw Header 提取 offset 即可尝试读 Slot - -**SlotACapacity / SlotBCapacity (各 8B, offset 24/40)** -- uint64 最大值 16EB,远超 64MB LV 上限 -- 两个字段分开存储,为未来非对称 Slot 预留可能 - -**LastUpdateTime (8B, offset 48)** -- epoch 毫秒,uint64 -- 诊断用途:不读 Slot 即可判断最后更新时间 -- 冲突检测:多主脑裂时辅助判断数据新鲜度 - -**SchemaVersion (8B, offset 56)** -- Payload JSON 的业务 schema 版本,uint64 -- 读 Header 即可判断是否认识该版本,无需解码整个 Slot -- **编码规则**:将 `dbf.getDbVersion()` 返回的数据库版本字符串(如 `"4.10.12"`)解析为数字组件后压缩为 uint64:**`(A << 40) | (B << 20) | C`**。例如 `"4.10.12"` → `(4 << 40) | (10 << 20) | 12`。解码:`A = v >> 40`,`B = (v >> 20) & 0xFFFFF`,`C = v & 0xFFFFF`。每个组件 20 bit,最大支持 **1,048,575** -- Header 中的 SchemaVersion 与 DTO JSON 中的 `schemaVersion` 字符串(`dbf.getDbVersion()`)是同一语义的不同表示,写入时编码、读取时解码 -- 扩容理由:原 uint32 每段 10 bit(最大 1023),uint64 每段 20 bit,容量扩大一倍(bit 数),版本号空间充裕 -- **格式合约**:`dbf.getDbVersion()` 返回值必须匹配 `^\d+\.\d+\.\d+$`,每段 ≤ 1,048,575。不匹配时拒绝编码并记录 ERROR 日志 -- **Python 2 注意**:位移操作需用 long 字面量避免溢出,如 `4L << 40`。或统一使用 `int()` 包裹:`int(a) << 40 | int(b) << 20 | int(c)`。Python 2 的 `int` 在超过 `sys.maxint` 时自动提升为 `long`,但显式使用 `long()` 更安全 -- **字段迁移说明**:原控制区 Reserved(4B, offset 60) 的位置被 SchemaVersion 扩展吸收。控制区内的预留空间功能由 Header 预留区 [928, 4096)(3168B)承担,空间充裕 - -**ControlChecksum (32B, offset 64)** -- SHA-256 of bytes[0:64] -- 覆盖 Checksum 之前的所有控制字段(Magic 到 SchemaVersion,共 64B) -- **不覆盖 VM 摘要区和预留区**:职责分离,控制区和摘要区各自独立校验 -- 校验逻辑:`sha256(block[0:64]) == block[64:96]` - -**VmCategory (1B, offset 96)** -- VM Business 类别,用于批量扫描时快速分类筛选 -- 0=REGULAR(普通虚拟机,含链式克隆子 VM),1=TEMPLATE(模板 VM),2=TEMPLATE_CACHE(缓存 VM) -- 旧版本写入的 Header 此处为 0,解读为 REGULAR(向后兼容) -- 枚举值与 Java 侧 `VmMetadataCategory` 一致(见 [Part 1a §2.2](vm-metadata-01a-数据模型与序列化.md#22-vmmetadatacategory-枚举)) - -**VmUuid (32B, offset 97)** -- VM UUID 的 hex 字符串表示(32 字符 ASCII,无连字符) -- 扫描时无需解码 Slot 即可按 UUID 检索 -- 固定 32B,无需长度前缀 - -**VmNameLen (2B, offset 129)** -- VmName 字段的实际 UTF-8 字节长度 -- uint16 BE,最大 65535,远超 765B 上限 -- 0 表示未设置(旧版本兼容:旧 Header 此处为 0,意为"名称不可用,需读 Slot") - -**VmName (765B, offset 131)** -- VM 名称 UTF-8 编码,MySQL `varchar(255)` + `charset utf8` 最大 765 字节 -- **截断规则**:若 VM 名称的 UTF-8 字节数超出 765B,截断到 765B(在 UTF-8 字符边界截断,避免截断多字节字符的中间)。截断不影响 Slot 中的完整 JSON 中的 VM 名称 -- 尾部未使用空间零填充 - -**SummaryChecksum (32B, offset 896)** -- SHA-256 of bytes[96:896] -- 独立于 ControlChecksum,仅覆盖 VM 摘要字段 -- 扫描时校验失败 → 摘要不可信 → 需读 Slot 获取 VM 信息(降级但不影响数据正确性) -- 与 ControlChecksum 分离的理由:控制区是崩溃恢复的核心,不能因为摘要区写入异常导致控制区被判为无效 - -**Reserved (3168B, offset 928)** -- 零填充至 4096B -- 未来扩展空间充裕:可增加大量新字段而无需再次扩容 Header - -### 1.3 hexdump 示例 - -``` -// 控制区 [0, 64) -00000000 5a 53 4d 54 00 01 00 01 00 00 00 00 00 00 00 2a |ZSMT...........*| - ^^^^^^^^^ ^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ - Magic V=1 A=0 P=1 WriteSeq = 42 - -00000010 00 00 00 00 00 00 10 00 00 00 00 00 00 1f e0 00 |................| - ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ - SlotAOffset = 4096 SlotACapacity = 2088960 - -00000020 00 00 00 00 00 1f f0 00 00 00 00 00 00 1f e0 00 |................| - ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ - SlotBOffset = 2093056 SlotBCapacity = 2088960 - -00000030 00 00 01 8e 3a 5b c0 00 00 00 04 00 00 a0 00 0c |....:...........| - ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ - LastUpdate=1709123456000 SchemaVersion="4.10.12" - -// ControlChecksum [64, 96) -00000040 a1 b2 c3 d4 ... (32 bytes SHA-256 of [0:64]) ... |................| - -// VM 摘要区 [96, 896) -00000060 01 61 62 63 64 65 66 30 31 32 33 34 35 36 37 38 |.abcdef012345678| - ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Cat=1 VmUuid (前 15B) = "abcdef0123456789..." - -00000070 39 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 |901234567890abcde| - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - VmUuid (后 17B) = "...901234567890abcde" - -00000081 00 09 e6 b5 8b e8 af 95 56 4d 00 00 00 ... |........VM......| - ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - NameLen=9 VmName="测试VM" (UTF-8, 9 bytes) - - ... (VmName padding to offset 896) ... - -// SummaryChecksum [896, 928) -00000380 f1 e2 d3 c4 ... (32 bytes SHA-256 of [96:896]) ...|................| - -// 预留区 [928, 4096) -000003A0 00 00 00 00 ... (zero padding to 4096B) ... |................| -``` - -### 1.4 版本兼容策略 - -``` -读取时: - if header_version > MAX_KNOWN_VERSION: - → 拒绝解析,返回错误,提示升级软件 - - if header_version == 1: - → 用 V1 布局解析(当前方案) - - # 未来 V2 示例: - if header_version == 2: - → 控制区 Reserved 位置改为 CompressionType - → 预留区 offset 928~935 分配给新字段 - → ControlChecksum 范围不变(仍 bytes[0:64]) - → 新字段在预留区有各自独立校验或归入 SummaryChecksum -``` - ---- - -## 2. Slot 结构 - -Slot 是数据搬运工,职责单一:可靠地存取 payload、支持自描述恢复。 - -### 2.1 字段定义 - -``` -Offset Size Field Type Description -─────── ───── ────────────── ────────── ────────────────────────────────── -0 4B Magic uint32 BE 固定 0x5A534454 ("ZSDT") -4 8B SeqNum uint64 BE 写序号,与 Header.WriteSequence 对应 -12 8B SlotOffset uint64 BE 自描述:本 Slot 在 LV 中的字节偏移 -20 8B SlotCapacity uint64 BE 自描述:本 Slot 容量 -28 8B PayloadLen uint64 BE Payload 实际字节数 -─────── -36B (固定 Header 部分) -─────── -36 NB Payload raw bytes 元数据 DTO JSON 明文(systemTags/resourceConfigs 为 per-Resource Base64 编码) -36+N 32B Checksum raw bytes SHA-256(bytes[0:36+N]) -─────── -Total: 36 + N + 32 B -``` - -> **注**:Checksum 中 `bytes[0:36+N]` 的偏移量相对于 **Slot 起始位置**(即 `SlotOffset`),非 LV 起始位置。实现时应从 SlotOffset 开始读取 `36+N` 字节作为 Checksum 输入。 - -### 2.2 字段说明 - -| 字段 | 设计理由 | -|------|---------| -| Magic | 标识 Slot 数据块,brute-force 恢复的入口条件 | -| SeqNum | 与 Header.WriteSequence 匹配来判断 Phase 2 是否完成 | -| SlotOffset | Header 损坏时的自描述定位;brute-force 时 `stored_offset == actual_offset` 是强校验 | -| SlotCapacity | 配合 SlotOffset 可重建布局;`SlotA.Offset + SlotA.Capacity` 可定位 SlotB | -| PayloadLen | 8B (uint64),虽然实际不超过 32MB,但保持与其他字段统一的 8B 对齐 | -| Payload | 变长,元数据 DTO JSON(systemTags/resourceConfigs 字段为 per-Resource Base64 编码) | -| Checksum | 尾部放置,SHA-256 覆盖 SlotHeader + Payload 全部内容 | - -### 2.3 Checksum 放尾部的理由 - -| 方案 | 优点 | 缺点 | 结论 | -|------|------|------|------| -| Checksum 在尾部(当前) | 写入自然流程;覆盖全部数据 | 需先读 PayloadLen 才知道 Checksum 位置 | (Y) 采用 | -| Checksum 在 Header 固定位置 | 固定偏移 | 不读 Payload 也无法校验,没有实际收益 | (N) | -| Header/Payload 双 Checksum | 可先验证 Header | 增加写入复杂度,1MB 优化读已覆盖大多数场景 | (N) | - -### 2.4 校验清单 - -> 两种校验模式:正常路径 (strict) 用于常规读取,恢复路径 (relaxed) 用于 Header 损坏后的 Slot 恢复。 - -| 校验项 | strict(正常读取) | relaxed(恢复路径) | -|--------|-------------------|-------------------| -| Magic == 0x5A534454 | (Y) | (Y) | -| SlotOffset == expected | (Y) | (Y) | -| SlotCapacity == expected | (Y) | (N) 跳过(推算 capacity 可能不准) | -| PayloadLen 范围合理 | (Y) | (Y) | -| SHA-256 Checksum | (Y) | (Y) | - -### 2.5 不做修改的候选项 - -| 候选改进 | 结论 | 理由 | -|---------|------|------| -| PayloadLen 缩为 4B | (N) 不改 | 只省 4B,破坏 8B 对齐 | -| 增加 SlotIndex (A/B 标识) | (N) 不改 | SeqNum 已够判断顺序,SlotIndex 冗余 | -| 增加 Slot 独立版本号 | (N) 不改 | Header 的 HeaderVersion 已管控全局布局版本 | - -## 3. 约束与不変量 - -1. **控制区校验不変量**:`ControlChecksum = SHA-256(bytes[0:64])`,读取端必须先校验再使用控制字段。 -2. **摘要区校验不変量**:`SummaryChecksum = SHA-256(bytes[96:896])`;校验失败只影响摘要可用性,不影响控制区有效性判断。 -3. **PendingOp 安全不変量**:未知 `PendingOp`(3~255)必须按 `STORAGE_CHANGE` 保守处理,不得降级为配置类快速路径。 -4. **Slot 自描述不変量**:`SlotOffset` 与 `SlotCapacity` 必须可用于 Header 损坏场景的恢复定位。 -5. **兼容演进不変量**:新增字段优先使用 Header 预留区 [928,4096),避免破坏既有控制区校验边界。 diff --git "a/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" "b/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" deleted file mode 100644 index 5bedbcf43cd..00000000000 --- "a/docs/design/vm-metadata-04c-sblk\345\206\231\345\205\245\346\265\201\347\250\213.md" +++ /dev/null @@ -1,268 +0,0 @@ -# VM 元数据 — sblk 写入流程 - -## 1. 设计原则 - -1. **Phase 1 不破坏现状**:写入的 Header 保留 Active Slot 的完整定位能力(布局字段 = 旧值) -2. **Phase 3 一次性提交**:ActiveSlot 切换、布局更新、PendingOp 清除 + VM 摘要更新在同一次 4KB Header 写入中完成 -3. **Slot 自描述**:每个 Slot 内嵌位置信息,即使 Header 损坏也可恢复 -4. **崩溃安全模型**:见 Part 4a §5.3 - ---- - -## 2. op_type 决策机制 - -> op_type 由控制面指定,Agent 端直接使用。 - -管理层面调用 `writeMetadata(payload, storageStructureChange)` 时显式指定 op_type: - -| 控制面输入 | Agent 端映射 | PendingOp 值 | -|-----------|-------------|-------------| -| `storageStructureChange = false` | CONFIG_UPDATE | 1 | -| `storageStructureChange = true` | STORAGE_CHANGE | 2 | - -**控制面决策规则**: - -| 场景 | storageStructureChange | 来源 | -|------|----------------------|------| -| `@MetadataImpact(CONFIG)` API(CPU/内存/标签等) | `false` | 注解 | -| `@MetadataImpact(STORAGE)` API(磁盘挂载/卸载、快照等) | `true` | 注解 | -| Full-refresh / 首次写入 | `true` | 控制面显式指定 | -| 多次 `markDirty` 合并 | OR 升级(任一为 true 则 true) | Poller 批量处理 | - -**好处**: -- 控制面对 op_type 拥有完整语义信息(知道哪个 API 触发了变更) -- Agent 无需读取旧 payload 做 diff,减少一次 I/O -- `VmMetadataDirtyVO` 记录 `storageStructureChange` 字段,Poller 批量处理时直接使用 - ---- - -## 3. 完整流程 - -### 3.1 前置步骤 - -``` -target_slot = 1 - Header.ActiveSlot -new_seq = Header.WriteSequence + 1 - -如果 payload 超出当前 Slot 容量: - new_lv_size = calculate_extend_size(current_lv_size, required) - 执行 lvextend(详见 Part 4e §2.4) - new_layout = calculate_slot_layout(new_lv_size) -否则: - new_layout = 当前 Header 中的布局(offset + capacity 不变) -``` - -### 3.2 Phase 1 — Mark Intent (4KB Header 写入) - -``` -写入 Header(4096B): - - 控制区 [0, 64): - Magic = 0x5A534D54 (不变) - HeaderVersion = 当前版本 (不变) - ActiveSlot = 旧值 ← 不切换 - PendingOp = op_type (1 或 2) ← 标记意图 - WriteSequence = new_seq ← 递增 - SlotAOffset = 旧值 ← 不变 - SlotACapacity = 旧值 ← 不变 - SlotBOffset = 旧值 ← 不变 - SlotBCapacity = 旧值 ← 不变 - LastUpdateTime = 旧值 ← 不变 - SchemaVersion = 旧值 ← 不变 - ControlChecksum = SHA-256(bytes[0:64]) - - VM 摘要区 [96, 928):保持旧值不变(Phase 1 不更新摘要) - SummaryChecksum = 旧值 ← 不重算 - 预留区 [928, 4096):零填充 - -关键约束:布局字段(Offset/Capacity)和 VM 摘要全部保持旧值 -理由:确保崩溃后 Active Slot 的定位信息完好;摘要在 Phase 3 统一更新 -``` - -### 3.3 Phase 2 — Write Payload - -``` -目标 Slot = target_slot -使用 new_layout 中的 offset/capacity - -写入 Slot 数据: - SlotHeader: - Magic = 0x5A534454 - SeqNum = new_seq - SlotOffset = new_layout 中目标 slot 的 offset - SlotCapacity = new_layout 中目标 slot 的 capacity - PayloadLen = len(payload) - Payload: - 元数据 DTO JSON(systemTags/resourceConfigs 为 per-Resource Base64) - Checksum: - SHA-256(SlotHeader + Payload) - -写入按 ALIGNMENT(4096) 对齐,零填充 -``` - -### 3.4 Phase 3 — Commit (4KB Header 写入) - -``` -写入 Header(4096B): - - 控制区 [0, 64): - Magic = 0x5A534D54 (不变) - HeaderVersion = 当前版本 (不变) - ActiveSlot = target_slot ← 切换 - PendingOp = 0 ← 清除 - WriteSequence = new_seq ← 保持 Phase 1 值 - SlotAOffset = new_layout 值 ← 此时更新 - SlotACapacity = new_layout 值 ← 此时更新 - SlotBOffset = new_layout 值 ← 此时更新 - SlotBCapacity = new_layout 值 ← 此时更新 - LastUpdateTime = now() ← 此时更新 - SchemaVersion = 当前 schema 版本 ← 此时更新 - ControlChecksum = SHA-256(bytes[0:64]) - - VM 摘要区 [96, 928): - VmCategory = vm_category ← 此时更新 - VmUuid = vm_uuid ← 此时更新(首次写入后不变) - VmNameLen = len(vm_name_utf8) ← 此时更新 - VmName = vm_name_utf8 ← 此时更新 - SummaryChecksum = SHA-256(bytes[96:896]) - - 预留区 [928, 4096):零填充 - -关键:ActiveSlot 切换 + 布局更新 + PendingOp 清除 + VM 摘要更新 - 在同一次 4KB Header O_DIRECT 写入中完成。 -``` - -### 3.5 Header 字段变更对照表 - -| 字段 | Phase 1 | Phase 3 | -|------|---------|---------| -| Magic | 不变 | 不变 | -| HeaderVersion | 不变 | 不变 | -| ActiveSlot | **不变**(旧值) | **切换**(target) | -| PendingOp | **设置**(op_type) | **清除**(0) | -| WriteSequence | **递增**(new_seq) | 不变(保持 new_seq) | -| SlotAOffset | **不变**(旧值) | **更新**(new_layout) | -| SlotACapacity | **不变**(旧值) | **更新**(new_layout) | -| SlotBOffset | **不变**(旧值) | **更新**(new_layout) | -| SlotBCapacity | **不变**(旧值) | **更新**(new_layout) | -| LastUpdateTime | **不变**(旧值) | **更新**(now) | -| SchemaVersion | **不变**(旧值) | **更新**(当前版本) | -| ControlChecksum | 重算 | 重算 | -| VM 摘要区 | **不变** | **更新** | -| SummaryChecksum | **不变** | **重算** | - ---- - -## 4. 崩溃场景分析 - -### 4.1 崩溃分析表 - -| 崩溃点 | Header 状态 | Active Slot | Target Slot | 恢复行为 | 结果 | -|--------|------------|-------------|-------------|----------|------| -| Phase 1 之前 | 旧值,pending=0 | 有效 | 旧/空 | 正常读 Active | (Y) 读旧数据 | -| Phase 1 之后,Phase 2 之前 | pending=op, seq=new, **布局=旧** | 有效(旧布局定位正确) | 旧/空 | 用旧布局找 Target → SeqNum≠new_seq → 回退 Active | (Y) 读旧数据 | -| Phase 2 进行中 | pending=op, seq=new, **布局=旧** | 有效 | 损坏(partial write) | 用旧布局找 Target → Checksum fail → 回退 Active | (Y) 读旧数据 | -| Phase 2 完成,Phase 3 之前 (无 extend) | pending=op, seq=new, **布局=旧** | 有效 | 有效,在旧布局位置 | 用旧布局找 Target → SeqNum==new_seq → 使用新数据 | (Y) NEED_REPAIR + 读新数据(Phase 2 数据有效,需 repair 完成 Phase 3) | -| Phase 2 完成,Phase 3 之前 (有 extend) | pending=op, seq=new, **布局=旧** | 有效 | 有效,但在新布局位置 | 用旧布局找 Target → 旧位置无有效数据 → 回退 Active | (!) 读旧数据;但 repair_pending_op 双布局尝试可恢复新数据(详见 Part 4d §4.5) | -| Phase 3 之后 | 全新值,pending=0 | 新 Active 有效 | — | 正常读新 Active | (Y) 读新数据 | - -### 4.2 LV extend + 崩溃场景详细分析 - -**场景:ActiveSlot=1(B),payload 太大触发 extend** - -``` -初始状态: - LV = 4MB - SlotA: offset=4096, cap=2044KB - SlotB: offset=2MiB+4096, cap=2044KB - ActiveSlot = 1 (Slot B) - -写入操作: - target = Slot A (inactive) - extend LV → 8MB - new_layout: SlotA offset=4096, cap=4MB; SlotB offset=4MB+4096, cap=4MB - -Phase 1: 写 Header - PendingOp=op, WriteSeq=new - SlotAOffset=4096, SlotACap=2044KB ← 旧值! - SlotBOffset=2MiB+4096, SlotBCap=2044KB ← 旧值! - -Phase 2: 写 payload 到 Slot A - 使用 new_layout: offset=4096, cap=4MB - -崩溃!Phase 3 未执行 -``` - -**恢复:** -- Header 中 ActiveSlot=1 → 读 Slot B -- SlotBOffset=2MiB+4096(旧值)→ Slot B 数据在该位置 → **定位正确** (Y) -- 读到旧数据,返回 NEED_REPAIR 或 STORAGE_CHANGE_INCOMPLETE - -**对比旧方案(不修复的情况):** -- 旧方案 Phase 1 会写新 capacity → SlotBOffset = 4096+4MB → Slot B 实际数据在 2MiB+4096 → **定位失败** (N) - -### 4.3 extend 场景丢失写入的权衡 - -> **扩容场景修复**:若 Phase 1 完成后 LV 已扩容且 Phase 2 已用新布局写入,repair_pending_op 需尝试双布局恢复。详见 [Part 4d §repair](vm-metadata-04d-sblk读取与恢复.md#repair_pending_op)。 - -**丢失发生条件(必须同时满足):** -1. 本次写入触发了 LV extend -2. 崩溃恰好发生在 Phase 2 完成后、Phase 3 执行前 - -**为什么可以接受:** -- 数据安全:旧数据完整可读,不损失已提交数据 -- 语义正确:Phase 3 未完成 = 事务未提交 = 丢弃未提交数据是正确行为 -- 自动恢复:management plane 检测到 pending_op 后会重试或 repair -- 概率极低:extend 不频繁(4MB→64MB 最多几次),且崩溃恰好卡在极窄窗口 - -**替代方案评估:** - -| 方案 | 可行性 | 问题 | -|------|--------|------| -| Phase 2 写入旧布局位置 | (N) | 旧容量不够(否则不需要 extend) | -| 四阶段写入(Phase 2.5 更新布局) | (N) | Phase 2.5 崩溃后回到同样问题 | -| Write Ahead Log | (N) | 过度设计,复杂度与收益不对等 | - -**结论:接受此场景下的行为,三阶段足够。** - ---- - -## 5. 完整状态转换图 - -``` - ┌──────────────┐ - │ PendingOp=0 │ 正常状态 - │ ActiveSlot=X│ - └──────┬───────┘ - │ - write_metadata() - │ - ┌────────────▼────────────┐ - │ Phase 1 │ - │ PendingOp=1或2 │ - │ WriteSeq=new │ - │ ActiveSlot=X (不变) │ - │ Layout=旧 (不变) │ - └────────────┬─────────────┘ - │ - ┌──────▼──────┐ - ┌───────│ Phase 2 │───────┐ - │ │ Write Slot │ │ - │ └──────┬──────┘ │ - │ │ │ - 崩溃(Target无效) 崩溃(Target有效) 正常 - │ │ │ - ▼ ▼ ▼ - ┌──────────┐ ┌───────────┐ ┌──────────────┐ - │回退到旧 │ │NEED_REPAIR│ │ Phase 3 │ - │Active │ │可用新数据 │ │ Commit │ - └────┬─────┘ └─────┬─────┘ └──────┬───────┘ - │ │ │ - ┌────▼─────┐ ┌─────▼─────┐ ┌──────▼───────┐ - │若op=1: │ │ repair: │ │ PendingOp=0 │ - │清除→OK │ │完成Phase3 │ │ ActiveSlot=Y │ - │若op=2: │ │ │ │ Layout=新 │ - │不清除→ │ └───────────┘ └──────────────┘ - │需refresh │ 正常状态 - └──────────┘ -``` diff --git "a/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" "b/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" deleted file mode 100644 index 8edc2091ff6..00000000000 --- "a/docs/design/vm-metadata-04d-sblk\350\257\273\345\217\226\344\270\216\346\201\242\345\244\215.md" +++ /dev/null @@ -1,441 +0,0 @@ -# VM 元数据 — sblk 读取与恢复 - -## 1. 读取主流程 - -``` -read_metadata(lv_path, lv_size): - - 1. 以 O_DIRECT | O_SYNC 只读打开 LV - 2. 读 Header Block (4KB) - 3. 反序列化 + 校验 Header(magic、version、ControlChecksum) - - 4. 如果 Header 有效: - 从 Header 直接读取 Slot 定位信息: - slot_a_off = Header.SlotAOffset ← 显式读取,不计算 - slot_a_cap = Header.SlotACapacity - slot_b_off = Header.SlotBOffset ← 显式读取,不计算 - slot_b_cap = Header.SlotBCapacity - - 根据 PendingOp 分支 → Flow A / B / C - - 5. 如果 Header 无效: - → 进入恢复流程(§3) -``` - ---- - -## 2. 三种读取分支 - -### 2.1 Flow A — PendingOp = 0(正常) - -``` -读 Active Slot → 校验通过 → 返回 OK + payload - -如果 Active Slot 校验失败: - 读 Inactive Slot - 如果 Inactive 有效: - → DEGRADED + payload(inactive) - → is_usable = True(允许灾备注册) - → warning: "Active 损坏,已降级使用 Inactive,数据可能落后一写入周期" - → repair_action: 后台触发 repair(优先切换 Active;必要时 full-refresh) - 如果 Inactive 也无效: - → CORRUPTED + "两个 Slot 均损坏" - → repair_action: full-refresh -``` - -### 2.2 Flow B — PendingOp = 1(CONFIG_UPDATE 中断) - -CONFIG_UPDATE 的特点:旧数据可以安全使用(只是配置过时,不会导致数据损坏)。 - -``` -target_slot = 1 - ActiveSlot - -尝试读 Target Slot: - 如果有效 且 SeqNum == Header.WriteSequence: - → Phase 2 已完成,Phase 3 未完成 - → NEED_REPAIR + target 的 payload(更新的数据) - → repair_action: 完成 Phase 3 - - 否则(Target 无效或 SeqNum 不匹配): - 回退读 Active Slot: - 如果有效: - → NEED_REPAIR + active 的 payload(旧但安全的数据) - → repair_action: 清除 PendingOp - 如果无效: - → CORRUPTED -``` - -### 2.3 Flow C — PendingOp = 2(STORAGE_CHANGE 中断) - -STORAGE_CHANGE 的特点:存储操作可能已在块设备层面完成,旧元数据描述的存储拓扑与实际不符,**使用旧元数据注册 VM 可能导致数据丢失**。 - -``` -target_slot = 1 - ActiveSlot - -尝试读 Target Slot: - 如果有效 且 SeqNum == Header.WriteSequence: - → Phase 2 已完成,Phase 3 未完成 - → NEED_REPAIR + target 的 payload(新拓扑数据) - → repair_action: 完成 Phase 3 - → 这是安全的,新数据反映了存储变更 - - 否则(Target 无效或 SeqNum 不匹配): - → Phase 2 未完成或数据损坏 - → 旧 Active Slot 的数据已过期,不反映当前存储状态 - - 读 Active Slot(仅用于诊断,标记为 stale): - → STORAGE_CHANGE_INCOMPLETE - → payload = active 的旧数据(标记为 stale) - → is_usable() = False ← 关键:禁止正常使用 - → error: "存储拓扑已变更但元数据未更新,必须执行 full-refresh" - → repair_action: "从数据库重建元数据,执行 full-refresh" -``` - -> **H2 修复 — 管理面自愈链路**:当控制面收到 `STORAGE_CHANGE_INCOMPLETE` 状态时,执行 `markDirty(vmUuid, true)`(`storageStructureChange=true`)触发 Poller 全量重建。若 Poller 重试耗尽,dirty 行被删除的同时在 `VmMetadataPathFingerprintVO` 上标记 `lastFlushFailed=true`。独立的 `MetadataStaleRecoveryTask`(每 30 分钟)扫描该标记并重新 `markDirty()`,为低频 VM 提供持续自愈能力。详见 [Part 2 §4.8](vm-metadata-02-脏标记与Poller.md#48-stale-恢复任务h2-修复)。 - -### 2.4 ReadResult 状态语义 - -| Status | payload | is_usable() | 调用方行为 | -|--------|---------|-------------|-----------| -| OK | (Y) 有效 | True | 正常使用 | -| NEED_REPAIR | (Y) 有效 | True | 使用数据 + 触发后台 repair | -| RECOVERED | (Y) 有效 | True | 使用数据 + 触发 Header 重建 | -| DEGRADED | (!) 有效(非最新) | True | 允许继续(如灾备注册)+ 必须告警 + 触发 repair | -| STORAGE_CHANGE_INCOMPLETE | (!) stale 数据 | **False** | **禁止注册 VM**,必须 full-refresh | -| CORRUPTED | (N) 无 | False | 必须 full-refresh | - -### 2.5 Slot 读取优化 - -``` -optimistic_read_size = min(slot_capacity, 1MB) - -第一次读: 从 slot_offset 读 optimistic_read_size - → 大多数情况下 payload < 1MB,一次读取完成 - -如果 payload + header + checksum > optimistic_read_size: - 第二次读: 从 slot_offset 读 aligned_up(total_needed) - → 仅在极大 payload 时触发 -``` - ---- - -## 3. Header 损坏恢复 - -当 Header 校验失败(magic 错误、checksum 不匹配、version 不认识)时,进入分层恢复。 - -### 3.1 恢复层次总览 - -``` -Layer 1: Raw Header 字段提取 - │ 即使 ControlChecksum 校验失败,Header 控制区 [0, 64) 的字段可能仍可读 - │ 尝试提取 ActiveSlot、SlotAOffset、SlotBOffset 等 - │ 比无 Offset 字段时多了直接定位信息,恢复成功率更高 - │ - ▼ -Layer 2: 布局推算 - │ 用 `blockdev --getsize64 /dev/{vg}/{lv}` 获取当前 LV 实际大小 - │ 先尝试当前 lv_size 对应布局,再穷举 KNOWN_LV_SIZES 的历史布局 - │ KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] - │ 每个布局最多探测 A/B 两个 Slot(最多 18 次 I/O 探测) - │ 命中任一可校验 Slot 即返回该布局候选 - │ - ▼ -Layer 3: Slot A 自描述辅助定位 Slot B - │ 如果 Layer 2 的 Slot B 位置失败 - │ 从 Slot A 的 SlotOffset + SlotCapacity 推算旧 Slot B 位置 - │ 覆盖 LV extend 后布局变化的情况 - │ - ▼ -Layer 4: Brute-force 扫描 - 最后手段,以 1MB 为单位批量读取 LV,在内存中逐 ALIGNMENT 对齐位置搜索 - 匹配条件:ZSDT Magic + SlotOffset == actual_offset(双重校验,误报极低) - 64MB LV ≈ 64 次 × 1MB 读 ≈ 64MB I/O(顺序读,SSD 场景 <1s) -``` - -### 3.2 Slot 选择策略 - -当找到两个有效 Slot 时: - -``` -优先级: - 1. Raw Header 中的 ActiveSlot hint(如果可提取)→ 使用 hint 指向的 Slot - 2. 无 hint → 使用 SeqNum 更高的 Slot(最后写入的数据更新) - 3. 只有一个有效 → 使用该 Slot - 4. 都无效 → CORRUPTED - -注意:恢复路径使用 relaxed 校验模式(见 Part 4b §2.4) - - 不校验 SlotCapacity(因为传入的 capacity 可能是推算的,与 Slot 自描述不同) - - 依赖 Checksum 作为最终数据完整性裁判 -``` - -### 3.3 Layer 2 多布局穷举(Q4-1) - -``` -KNOWN_LV_SIZES = [4MB, 6MB, 8MB, 12MB, 16MB, 24MB, 32MB, 48MB, 64MB] - -输入: current_lv_size -候选集合: [size in KNOWN_LV_SIZES where size <= current_lv_size] - -对每个候选 size: - 1. layout = calculate_slot_layout(size) - 2. 尝试读取 slotA/slotB 的 slot header magic + checksum - 3. 若任一 slot 可校验通过,记录该 layout 为可用候选 - -选择策略: - - 优先命中 Header hint 指向的 slot/layout - - 否则按 SeqNum 选择更新数据 - -复杂度上界: - - 候选布局最多 9 个 - - 每布局最多 2 次 slot 探测 - - 总探测 ≤ 18 次 I/O(不含最终 payload 读取) -``` - -### 3.4 Layer 1 详细逻辑 - -``` -读取 Header 原始 4KB 数据(控制区字段在 [0, 64) 内) - -尝试解析 Magic: - if magic != 0x5A534D54 → 跳过 Layer 1,进入 Layer 2 - -Magic 正确但 Checksum 错误(单 bit 翻转等场景): - 提取各字段作为 hint: - active_slot_hint ← 如果值 ∈ {0, 1} 则可信 - slot_a_off_hint ← 如果值 > 0 且 < lv_size 则可用 - slot_b_off_hint ← 如果值 > slot_a_off_hint 且 < lv_size 则可用 - - 用 hint 的 offset 尝试读 Slot: - 如果成功 → 返回 RECOVERED - 如果失败 → 继续 Layer 2 -``` - -**Layer 1 的改进**:Header 显式存储 SlotAOffset + SlotBOffset,raw 提取后直接可用,无需从 SlotACapacity 间接推算,减少一步出错风险。 - ---- - -## 4. PendingOp 语义与 Repair 策略 - -### 4.1 PendingOp 语义对照 - -| PendingOp | 含义 | 写入中断的后果 | 旧数据安全性 | 可否简单清除 | -|-----------|------|--------------|-------------|-------------| -| 0 | 空闲,上次写入已完成 | — | — | — | -| 1 (CONFIG_UPDATE) | 正在写入普通配置变更的元数据 | 丢失一次配置更新,可接受 | (Y) 旧配置安全可用 | (Y) 可以 | -| 2 (STORAGE_CHANGE) | 正在写入存储变更后的元数据(存储上已有新快照/卷) | 存储上有新数据,但元数据没记录! | (N) 旧拓扑与实际不符 | (N) **绝不可以** | - -**核心区别**:CONFIG_UPDATE 的旧数据"过时但安全",STORAGE_CHANGE 的旧数据"过时且危险"。 - -### 4.2 repair_pending_op — CONFIG_UPDATE (pending_op = 1) - -``` -读取 Header → 确认 pending_op = 1 - -先用 Header 旧布局计算 target_slot 并读取 - -如果旧布局失败,再用当前 LV 大小推导新布局重试 target_slot -(双布局尝试:old-layout → new-layout) - -Case A: Target 有效 且 SeqNum == Header.WriteSequence - → Phase 2 已完成,只需完成 Phase 3 - → 写入新 Header: - ActiveSlot = target_slot - PendingOp = 0 - WriteSequence = 保持 - 布局字段 = 若命中旧布局则保持旧值,若命中新布局则更新为新布局 - LastUpdateTime = now() - → 返回 repaired=True, "Completed Phase 3" - -Case B: Target 无效(旧/新布局均失败) - → Phase 2 未完成(或数据损坏) - → 安全丢弃本次写入,恢复到旧状态 - → 写入新 Header: - ActiveSlot = 保持(旧值) - PendingOp = 0 ← 清除 - WriteSequence = 保持 - 布局字段 = 保持 - LastUpdateTime = 保持 - → 返回 repaired=True, "Aborted incomplete config update" -``` - -### 4.3 repair_pending_op — STORAGE_CHANGE (pending_op = 2) - -``` -读取 Header → 确认 pending_op = 2 - -先用 Header 旧布局计算 target_slot 并读取 - -如果旧布局失败,再用当前 LV 大小推导新布局重试 target_slot -(双布局尝试:old-layout → new-layout) - -Case A: Target 有效 且 SeqNum == Header.WriteSequence - → Phase 2 已完成,可以安全完成 Phase 3 - → 写入新 Header: - ActiveSlot = target_slot - PendingOp = 0 - WriteSequence = 保持 - 布局字段 = 若命中新布局则更新为新布局,否则保持旧值 - LastUpdateTime = now() - → 返回 repaired=True, "Completed Phase 3 for storage change" - -Case B: Target 无效(旧/新布局均失败) - → Phase 2 未完成 - → 旧 Active Slot 中的元数据不反映当前存储状态 - → (!) 不清除 PendingOp ← 关键决策 - → 返回 repaired=False, - error="STORAGE_CHANGE pending, target data lost. - Metadata is stale. Must execute full-refresh - from database to rebuild metadata." -``` - -### 4.4 为什么 STORAGE_CHANGE 不能简单清除 PendingOp - -``` -如果清除 pending_op: - Header 变为: pending=0, ActiveSlot=旧 - 后续 read_metadata → 返回 OK + 旧 payload - 调用方认为数据有效 → 用旧拓扑注册 VM - - 但实际存储状态已变更(如:快照已创建/删除、卷已扩容) - 旧拓扑 ≠ 当前存储 → VM 挂载错误的快照链 - → 数据损坏或丢失 -``` - -**PendingOp=2 是一个"脏标记"**:它的存在持续提醒系统"存储状态与元数据不一致"。只有两种方式可以消除该标记: - -1. **找到有效 Target 完成 Phase 3** — 新元数据反映了存储变更,安全 -2. **Full-refresh 写入全新元数据** — 从数据库重建完整拓扑,覆盖整个 Header - -### 4.5 双布局 repair 伪代码(Q4-5) - -```python -def repair_pending_op(header, current_lv_size): - target = 1 - header.active_slot - - # 尝试 1:Header 旧布局 - old_layout = header.layout - slot = try_read_target(target, old_layout) - if slot.valid and slot.seq_num == header.write_sequence: - return complete_phase3(header, slot, old_layout) - - # 尝试 2:当前 LV 新布局 - new_layout = calculate_slot_layout(current_lv_size) - slot = try_read_target(target, new_layout) - if slot.valid and slot.seq_num == header.write_sequence: - return complete_phase3(header, slot, new_layout) # 同步更新 Header 布局字段 - - # 双布局均失败 - if header.pending_op == STORAGE_CHANGE: - return RepairResult(repaired=False, keep_pending=True) - else: - return clear_pending_and_keep_active(header) -``` - ---- - -## 5. Full-Refresh 机制 - -### 5.1 触发条件 - -| 场景 | 触发方 | -|------|--------| -| STORAGE_CHANGE_INCOMPLETE | management plane 检测到后主动触发 | -| CORRUPTED(两个 Slot 都损坏) | management plane 检测到后主动触发 | -| repair_pending_op 返回 repaired=False | management plane 收到失败回调后触发 | -| 管理员手动触发 | 运维命令 | - -### 5.2 执行方式 - -Full-refresh 本质上是一次普通的 `write_metadata` 调用: - -``` -full_refresh(lv_path, lv_size_getter, lv_extend_func): - - 1. Management plane 从数据库查询 VM 的完整存储拓扑 - 2. 生成最新的 payload JSON - 3. 调用 write_metadata(lv_path, payload, storageStructureChange=True) - → 控制面显式指定 op_type = STORAGE_CHANGE (2) - - 写入流程: - Phase 1: PendingOp=2, WriteSeq=old+1 - Phase 2: 写入新 payload 到 inactive Slot - Phase 3: ActiveSlot 切换, PendingOp=0 - - 成功后: - - 旧的 STORAGE_CHANGE pending 状态被覆盖 - - 新元数据反映数据库中的最新拓扑 - - 两个 Slot 中至少有一个包含正确数据 -``` - -### 5.3 Full-refresh 使用 STORAGE_CHANGE(2) 的理由 - -- Full-refresh 由控制面触发,显式指定 `storageStructureChange=true` → op_type=2 -- 这自然解决了"full-refresh Phase 1 覆盖脏标记"问题:新的 PendingOp=2 与旧的语义一致 -- 不需要引入新的 OP_FULL_REFRESH (3) - -### 5.4 Full-refresh 中断场景 - -``` -如果 full-refresh 本身在 Phase 2 之前崩溃: - Phase 1 写入了 PendingOp=2 - Target 无效 - repair → Case B for STORAGE_CHANGE → 返回 STORAGE_CHANGE_INCOMPLETE - 此时 Active Slot 仍然是旧的 - - 是否有风险? - → management plane 知道 full-refresh 失败了 - (write_metadata 会抛异常),会重试。 - → 重试仍会使用 op=2(控制面显式指定),PendingOp 语义一致。 - → 只要 management plane 正确实现重试逻辑,不会误用旧数据。 -``` - ---- - -## 6. 部分写入安全性分析 - -> 完整分析见 [Part 4a §5.3](vm-metadata-04a-sblk存储协议概述.md#53-崩溃安全模型)。本节仅补充读取/恢复视角的关键结论。 - -本协议**不依赖单次 4KB I/O 的原子性**。即使 Header 的 4KB 写入在中途崩溃导致部分字段更新: - -- **ControlChecksum 不匹配** → 进入 Header 损坏恢复流程(§3) -- **Layer 1**:从 raw Header 提取 Slot 偏移量(Magic 正确时各字段大概率可读) -- **Layer 2**:从 `blockdev --getsize64` 获取的 `lv_size` 推算 Slot 布局 -- **兜底**:Slot 自描述 + SHA-256 Checksum 保证最终数据完整性 - -> 最坏情况(Header + 一个 Slot 都损坏)下,恢复流程仍能通过 Brute-force 扫描(Layer 4)找到另一个有效 Slot。 - ---- - -## 附录 A. 读取与恢复测试矩阵 - -| # | 场景 | Header 状态 | PendingOp | Active Slot | Inactive Slot | 预期结果 | -|---|------|-------------|-----------|-------------|---------------|----------| -| 1 | 正常读取 | 有效 | 0 | 有效 | — | OK + payload | -| 2 | Active 损坏(降级路径) | 有效 | 0 | 无效 | 有效 | DEGRADED + payload(inactive), is_usable=True | -| 3 | 两 Slot 损坏 | 有效 | 0 | 无效 | 无效 | CORRUPTED | -| 4 | CONFIG Phase 2 完成 | 有效 | 1 | 有效(旧) | 有效 + SeqMatch | NEED_REPAIR + 新 payload | -| 5 | CONFIG Phase 2 未完成 | 有效 | 1 | 有效(旧) | 无效 | NEED_REPAIR + 旧 payload | -| 6 | STORAGE Phase 2 完成 | 有效 | 2 | 有效(旧) | 有效 + SeqMatch | NEED_REPAIR + 新 payload | -| 7 | STORAGE Phase 2 未完成 | 有效 | 2 | 有效(旧) | 无效 | STORAGE_CHANGE_INCOMPLETE | -| 8 | Header Checksum 错误 | 无效 | — | 有效 | 有效 | RECOVERED (Layer 1/2) | -| 9 | Header Magic 错误 | 无效 | — | 有效 | — | RECOVERED (Layer 2/4) | -| 10 | 全新 LV(刚初始化) | 有效 | 0 | 有效(空 `{}`) | 零 | OK + `{}` | -| 11 | LV 扩容后旧 Header | 有效 | 0 | 有效 | — | OK(Slot offset 从 Header 读取,不依赖 lv_size) | -| 12 | 灾备复制中途快照 | 可能部分 | — | 有效 | 半写 | RECOVERED / NEED_REPAIR | -| 13 | Layer 2 多布局命中历史 4MB 布局 | Header 无效 | — | 旧布局有效 | — | RECOVERED(枚举命中,≤18 次探测) | -| 14 | Layer 2 当前布局失败→Layer 3 自描述成功 | Header 无效 | — | SlotA 有效 | SlotB 旧偏移 | RECOVERED(Layer 3) | -| 15 | repair_pending_op: old-layout 失败、new-layout 成功 | 有效 | 1/2 | 旧布局 target 无效 | 新布局 target 有效 + SeqMatch | repaired=True;若 pending=2 同步更新布局字段 | -| 16 | brute-force 扫描超时 | Header 无效 | — | 未定位 | 未定位 | CORRUPTED(返回超时错误,避免长时间阻塞) | -| 17 | extend+Phase 2 完成+初始读取(旧布局 Header) | 有效 | 1/2 | 有效(旧) | 有效但在新布局位置 | 回退 Active → NEED_REPAIR(op=1) / STORAGE_CHANGE_INCOMPLETE(op=2);后续 repair 双布局尝试可恢复 | - ---- - -## 7. 约束与不変量 - -| 约束 ID | 约束描述 | 违反后果 | 检查点 | -|---------|----------|----------|--------| -| C-RD | Flow A 中 Active 损坏且 Inactive 可校验时,必须返回 `DEGRADED` 且 `is_usable=True` | 可恢复数据被误判为不可用,灾备恢复失败 | §2.1 / §2.4 | -| C-RC | `STORAGE_CHANGE_INCOMPLETE` 必须保持 `is_usable=False`,禁止注册路径消费 | 用 stale 拓扑注册 VM,可能造成数据损坏 | §2.3 / §2.4 | -| C-SC | `STORAGE_CHANGE_INCOMPLETE` 必须通过 `markDirty(vmUuid, true)` 触发全量重建,且重试耗尽后由 `MetadataStaleRecoveryTask` 接管恢复,禁止静默放弃 | 低频 VM 存储拓扑与元数据永久不一致 | §2.3 / Part 2 §4.8 | -| C-RP | `repair_pending_op` 必须按 old-layout → new-layout 双布局尝试;pending=2 双失败时不得清除 PendingOp | extend+Phase2 完成场景丢失可恢复数据,或错误掩盖存储变更脏态 | §4.3 / §4.5 | -| C-SV | Layer 2 多布局穷举集合固定为 9 种 `KNOWN_LV_SIZES`,探测上界 ≤18 次 I/O | 恢复复杂度失控或遗漏历史布局导致恢复失败 | §3.1 / §3.4 | diff --git "a/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" "b/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" deleted file mode 100644 index c6c82370d99..00000000000 --- "a/docs/design/vm-metadata-04e-sblk\350\277\220\347\273\264\344\270\216IO.md" +++ /dev/null @@ -1,432 +0,0 @@ -# VM 元数据 — sblk 运维与 I/O 细节 - -## 1. LV 命名与扫描 - -### 1.1 命名规范 - -``` -格式: {vm_uuid}_vmmeta -示例: a1b2c3d4e5f6_vmmeta -路径: /dev/{vg_uuid}/{vm_uuid}_vmmeta -``` - -### 1.2 扫描规则 - -``` -scan_metadata_lvs(vg_path, lv_list_func): - 遍历 VG 中所有 LV - 筛选 lv_name.endswith('_vmmeta') - 返回 [{vm_uuid, lv_path, lv_size}, ...] -``` - -**大规模扫描优化**(VM 数量 > 500 时建议启用): - -| 优化手段 | 说明 | -|----------|------| -| 仅读 Header 4KB | 扫描阶段只读 Header 获取 VM 摘要(`VmUuid`/`VmName`/`VmCategory`),不读 Slot payload,单次 I/O = 4KB | -| 并行 I/O | 多个 LV 的 Header 读取可并行执行(线程池并发度受 Agent 线程数控制,默认 10) | -| SummaryChecksum 降级 | 摘要校验失败时标记 `summary_valid=false`,不在扫描阶段触发 Slot 读取 | - ---- - -## 2. LV 容量管理 - -### 2.1 基本参数 - -| 参数 | 值 | 说明 | -|------|-----|------| -| 初始大小 | 4 MB | 足够绝大多数 VM 配置 | -| 最大大小 | 64 MB | 防止单 VM 元数据占用过多空间 | -| 对齐粒度 | 4096 B (ALIGNMENT) | 满足 O_DIRECT 对齐要求 | - -### 2.2 空间分配公式 - -``` -calculate_slot_layout(lv_size): - - header_reserved = ALIGNMENT (4096 B) ← Header Block = 4KB - available = lv_size - header_reserved - slot_capacity = (available / 2) 向下对齐到 ALIGNMENT - - slot_a_offset = header_reserved ← 固定 4096 - slot_a_capacity = slot_capacity - slot_b_offset = header_reserved + slot_capacity - slot_b_capacity = slot_capacity - -示例 (4MB LV): - header_reserved = 4096 - available = 4194304 - 4096 = 4190208 - slot_capacity = (4190208 / 2) 对齐 = 2093056 (≈ 2044 KB) - slot_a_offset = 4096 - slot_b_offset = 4096 + 2093056 = 2097152 -``` - -Slot 最大 payload = slot_capacity - 36 (SlotHeader) - 32 (Checksum) = slot_capacity - 68 - -### 2.3 阶梯扩容策略 - -当 payload 超出当前 Slot 容量时触发 LV 扩容。 - -#### 扩容步长 - -| 当前 LV 大小 | 步长 | -|-------------|------| -| < 8 MB | 2 MB | -| 8 MB ~ 16 MB | 4 MB | -| 16 MB ~ 32 MB | 8 MB | -| > 32 MB | 16 MB | - -#### 设计理由 - -- 小 LV 用小步长:避免浪费(大多数 VM 的元数据在 4MB 内就够了) -- 大 LV 用大步长:减少扩容次数(快照链很长的 VM 需要更多空间) -- 最大 64MB 上限:超过说明 VM 快照/卷数量异常,应在管理层面限制 - -#### 计算示例 - -``` -场景: 当前 LV=4MB, 需要 slot 容量 3MB - -required_lv = ALIGNMENT + 2 * align_up(3MB + 68B) ≈ 6MB + 4KB -当前 4MB < required 6MB - -step 1: 4MB + 2MB = 6MB → 仍 < 6MB+4KB -step 2: 6MB + 2MB = 8MB → 满足 -→ extend LV to 8MB -``` - -### 2.4 扩容时机与交互 - -``` -write_metadata() 中: - - required = SLOT_HEADER_SIZE + len(payload) + CHECKSUM_SIZE (= 36 + N + 32) - target_cap = Header 中 target slot 的 capacity - - if required > target_cap: - min_lv = ALIGNMENT + 2 * align_up(required, ALIGNMENT) - new_lv = calculate_extend_size(current_lv_size, min_lv) - lv_extend_func(new_lv) - 重新计算 new_layout -``` - -**扩容与三阶段写入的交互**(崩溃安全分析见 Part 4c §4.2): - -``` -关键:lvextend 后必须关闭并重新打开 fd - → 确保内核重新读取块设备大小,新增空间对后续 pwrite 可见 - → close(fd) → fd = open(lv_path, O_RDWR | O_DIRECT | O_SYNC) - -布局更新时序: - 扩容后计算 new_layout(新的 offset/capacity) - Phase 1: Header 中 布局字段 = 旧值(不更新) - Phase 2: payload 写入 new_layout 的 target 位置 - Phase 3: Header 中 布局字段 = new_layout(此时更新) -``` - -### 2.5 容量超限处理 - -``` -如果 required_lv > MAX_LV_SIZE (64MB): - → 抛出异常 - → 提示 "VM 元数据超过 64MB 上限,可能快照/卷数量异常" - → 管理层面应限制: - - 单 VM 快照数量上限 - - 定期清理过期快照 - - 合并快照链 -``` - ---- - -## 3. LV 生命周期 - -### 3.1 LV 初始化 - -> **设计变更**:LV 初始化时同时写入 Header + 空 Slot A(`payload="{}"`),并执行 O_DIRECT sanity check。 - -```python -def initialize_metadata_lv(lv_path, lv_size): - fd = os.open(lv_path, os.O_RDWR | os.O_DIRECT | os.O_SYNC) - try: - # Step 0: O_DIRECT sanity check - _io_sanity_check(fd) - - layout = calculate_slot_layout(lv_size) - - # Step 1: Build empty payload Slot A - empty_payload = b'{}' - slot_a = build_slot( - magic=SLOT_MAGIC, - seq_num=1, - slot_offset=layout.slot_a_offset, - slot_capacity=layout.slot_a_capacity, - payload=empty_payload - ) - - # Step 2: Write Slot A - write_aligned(fd, layout.slot_a_offset, slot_a) - - # Step 2.5: Clear Slot B region (zero-fill) - # 确保初始化后 Slot B 为全零,避免残留数据干扰恢复流程判断 - zero_buf = b'\x00' * layout.slot_b_capacity - write_aligned(fd, layout.slot_b_offset, zero_buf) - - # Step 3: Write Header (ActiveSlot=0, WriteSequence=1, PendingOp=0) - header = build_header( - active_slot=0, pending_op=0, write_sequence=1, - slot_a_offset=layout.slot_a_offset, - slot_a_capacity=layout.slot_a_capacity, - slot_b_offset=layout.slot_b_offset, - slot_b_capacity=layout.slot_b_capacity, - last_update_time=0, schema_version=0 - ) - write_aligned(fd, 0, header) - finally: - os.close(fd) -``` - -**O_DIRECT sanity check**: - -```python -def _io_sanity_check(fd): - """Verify O_DIRECT I/O path works correctly. - - 注意:sanity check 将测试数据写入 offset 0,后续 initialize_metadata_lv() - 会在同一位置写入正式 Header,自然覆盖测试数据。如果 Header 写入失败, - offset 0 处残留测试数据(Magic ≠ 0x5A534D54),读取时将进入恢复流程 - (预期行为:初始化未完成 = 无有效元数据)。 - """ - test_data = b'ZSMT_IO_CHECK' + b'\x00' * (4096 - 13) - with AlignedBuffer(4096) as buf: - buf.fill(test_data) - buf.pwrite(fd, 0) - with AlignedBuffer(4096) as buf: - buf.pread(fd, 0) - if buf.read(13) != b'ZSMT_IO_CHECK': - raise MetadataIOError("O_DIRECT sanity check failed on %s" % lv_path) -``` - -- sanity check 失败 → 抛异常 → 管理层面将此 PS 标记为"不支持元数据" → 该 PS 上所有 VM 静默跳过元数据写入 -- 首次 `read_metadata()` → 返回 `OK` with `payload="{}"`(而非 `CORRUPTED`),避免首次读取返回 CORRUPTED 与真正损坏混淆 - -### 3.2 LV 删除 - -``` -delete_metadata(lv_path, lv_delete_func): - 直接调用 lv_delete_func(lv_path) 删除整个 LV - 无需清理内部数据 -``` - ---- - -## 4. 健康检查 - -``` -get_metadata_status(lv_path): - 只读打开 LV → 读 Header 4KB → 校验 → 返回摘要 - - 返回值: - { - valid: bool - header_version: int - active_slot: int - pending_op: int - write_sequence: int - slot_a_offset: int - slot_a_capacity: int - slot_b_offset: int - slot_b_capacity: int - last_update_time: int - schema_version: int - vm_category: int - vm_uuid: str - vm_name: str - summary_valid: bool ← SummaryChecksum 校验结果 - } - - 用途: - - 运维巡检 - - 监控告警(pending_op != 0 持续时间过长) - - 诊断工具展示 -``` - -### 4.1 Layer 4 brute-force 扫描超时与日志(Q4-2) - -当 Header/常规布局恢复失败后,Layer 4 进入按步长扫描 Slot Magic 的 brute-force 路径。为避免底层 I/O 异常导致长时间阻塞,增加**全局 30 秒超时**与启动日志: - -```python -# 在 brute-force 扫描前 -BRUTE_FORCE_TIMEOUT_SEC = 30 -start_time = time.time() -log.info("Starting brute-force scan: LV size=%dMB, max_steps=%d", lv_size_mb, max_steps) - -# 在每次 pread 循环中检查 -if time.time() - start_time > BRUTE_FORCE_TIMEOUT_SEC: - log.error("Brute-force scan timed out after %ds", BRUTE_FORCE_TIMEOUT_SEC) - return CORRUPTED -``` - -实现约束: -- 该超时为**扫描全局超时**,不是单次 `pread` 超时。 -- 超时后直接返回 `CORRUPTED`,由上层走 full-refresh / 人工介入流程。 -- 日志中的 `lv_size_mb` 与 `max_steps` 必须在扫描开始时一次性打印,便于运维关联慢盘与异常设备。 - ---- - -## 5. I/O 技术细节 - -### 5.1 字节序 - -所有多字节整数字段统一使用**大端序(Big Endian)**。Python 2 中使用 `struct.pack('>I', magic)` / `struct.pack('>H', version)` / `struct.pack('>Q', seq_num)` 等。 - -选择大端序的理由: -- 大端序是网络字节序,跨平台数据交换的惯例 -- Magic Number `0x5A534D54` 在大端序下直接对应 ASCII "ZSMT",便于 hexdump 调试 -- LVM 元数据本身也使用大端序 - -### 5.2 SHA-256 输出格式 - -使用 **32 字节二进制**(非 64 字符十六进制字符串)。Python 2 中 `hashlib.sha256(data).digest()` 得到 32 字节 bytes。 - -### 5.3 O_DIRECT 与并发控制 - -**所有 sblk 元数据读写统一使用 `O_DIRECT | O_SYNC`**,包括 Header 和 Slot。理由: - -- sblk 是共享块设备,多节点可能访问同一 LV,page cache 会导致不一致 -- 元数据操作频率低(每次 API 后一次),性能开销可忽略 -- 代码路径统一,减少 bug 风险 -- 10MB O_DIRECT 顺序写入延迟:SSD 场景约 50ms,SAN 场景约 200ms,可接受 - -**前提条件:** -- Header 从 offset 0 开始,4KB 对齐 -- 使用 O_DIRECT + O_SYNC 确保不被 page cache 缓存(共享块设备多节点访问要求) -- 如果 LVM 能在该存储上正常工作(O_DIRECT 路径可用),则崩溃安全前提已满足 - -### 5.4 文件锁 - -**sblk 不使用文件锁**:共享块设备上 `fcntl.flock` 语义取决于具体实现(device-mapper + cluster),不可靠。sblk 场景的并发保护完全依赖管理平面的四层串行化机制(见 Part 2 §3.1)。即使毫秒级窗口的并发写入,因全量覆盖写语义,后者覆盖前者,结果依然正确(最终一致性)。无需引入额外分布式锁机制。 - -> local/NFS 使用 `fcntl.flock(fd, LOCK_EX | LOCK_NB)` 作为 defense-in-depth,在本地文件系统和 NFS 上语义可靠。NFS 的 `flock` 通过 NLM 协议实现,对同一 NFS server 上的多个客户端提供互斥语义。正常路径下不会有并发写入(管理面四层串行化已保证),flock 仅作为防御性保护防止异常重入。 - -### 5.5 O_DIRECT 内存对齐 - -O_DIRECT 要求用户态 buffer 地址和长度对齐到页大小(4KB)。`ctypes.create_string_buffer` 分配的内存**不保证**特定对齐。 - -**解决方案**:使用 `posix_memalign` + `ctypes` 封装为 `AlignedBuffer` 类。 - ---- - -## 6. AlignedBuffer 参考实现 - -> **Fallback 策略**:若 `posix_memalign` 不可用(极端环境),可 fallback 到 `mmap` + `MAP_ANONYMOUS` 分配页对齐内存。但 `posix_memalign` 在所有主流 Linux 发行版上均可用(glibc 2.0+),fallback 仅作防御性预留。 - -```python -import ctypes -import errno -import os - -_libc = ctypes.CDLL('libc.so.6', use_errno=True) - -class AlignedBuffer(object): - """Page-aligned buffer for O_DIRECT I/O. Use as context manager.""" - - def __init__(self, size, alignment=4096): - self._alignment = alignment - self._size = ((size + alignment - 1) // alignment) * alignment - self._ptr = ctypes.c_void_p() - ret = _libc.posix_memalign( - ctypes.byref(self._ptr), alignment, self._size) - if ret != 0: - raise OSError(ret, "posix_memalign failed") - # Zero-fill - ctypes.memset(self._ptr, 0, self._size) - - def fill(self, data, offset=0): - """Copy data into buffer at given offset.""" - ctypes.memmove(self._ptr.value + offset, data, len(data)) - - def read(self, length, offset=0): - """Read bytes from buffer.""" - return ctypes.string_at(self._ptr.value + offset, length) - - def pwrite(self, fd, file_offset): - """Write buffer contents to fd at file_offset using pwrite.""" - written = 0 - while written < self._size: - ret = _libc.pwrite( - fd, - self._ptr.value + written, - self._size - written, - ctypes.c_longlong(file_offset + written) - ) - if ret < 0: - err = ctypes.get_errno() - if err == errno.EINTR: - continue - raise OSError(err, "pwrite failed: " + os.strerror(err)) - if ret == 0: - raise IOError("pwrite returned 0 (disk full?)") - written += ret - - def pread(self, fd, file_offset): - """Read from fd at file_offset into buffer using pread.""" - read_bytes = 0 - while read_bytes < self._size: - ret = _libc.pread( - fd, - self._ptr.value + read_bytes, - self._size - read_bytes, - ctypes.c_longlong(file_offset + read_bytes) - ) - if ret < 0: - err = ctypes.get_errno() - if err == errno.EINTR: - continue - raise OSError(err, "pread failed: " + os.strerror(err)) - if ret == 0: - raise IOError("pread returned 0 (unexpected EOF)") - read_bytes += ret - - def close(self): - if self._ptr.value: - _libc.free(self._ptr) - self._ptr = ctypes.c_void_p() - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - def __del__(self): - self.close() -``` - -**使用示例**: - -```python -# 写入 Header (4KB) -with AlignedBuffer(4096) as buf: - buf.fill(header_bytes) - buf.pwrite(fd, 0) - -# 读取 Slot (up to 1MB optimistic) -with AlignedBuffer(1 * 1024 * 1024) as buf: - buf.pread(fd, slot_offset) - data = buf.read(expected_size) - -# 写入 Slot — 构造函数自动将 size 向上对齐到 alignment(4096) -# 注意:pwrite 始终写入 self._size(对齐后)字节,创建时应精确传入所需大小 -slot_total = SLOT_HEADER_SIZE + len(payload) + CHECKSUM_SIZE # 36 + N + 32 -with AlignedBuffer(slot_total) as buf: # e.g. 36+1000+32=1068 → 自动对齐为 4096 - buf.fill(slot_bytes) - buf.pwrite(fd, slot_offset) -``` - -## 7. 约束与不変量 - -1. **I/O 对齐不変量**:所有 Header/Slot 读写必须通过 4KB 对齐缓冲区执行,且使用 `O_DIRECT | O_SYNC`。 -2. **I/O 完整性不変量**:`pwrite/pread` 必须循环至 `self._size` 全部写完/读完;任何 short write/read 都不能被当作成功返回。 -3. **中断处理不変量**:遇到 `EINTR` 必须重试,不允许直接上抛导致部分数据路径中断。 -4. **扫描时延上限不変量**:Layer 4 brute-force 扫描总时长上限 30 秒,超时统一返回 `CORRUPTED`。 -5. **诊断可观测不変量**:brute-force 扫描开始时必须记录 `LV size` 和 `max_steps`,超时必须记录 ERROR。 diff --git "a/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" "b/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" deleted file mode 100644 index 383652a4cfd..00000000000 --- "a/docs/design/vm-metadata-05-API\350\256\276\350\256\241.md" +++ /dev/null @@ -1,521 +0,0 @@ -# VM 元数据 — API 设计 - -## 目录 - -1. [API 总览](#1-api-总览) -2. [扫描虚拟机元数据](#2-扫描虚拟机元数据) -3. [读取虚拟机元数据](#3-读取虚拟机元数据) -4. [注册虚拟机](#4-注册虚拟机) -5. [检查虚拟机元数据一致性](#5-检查虚拟机元数据一致性) -6. [运维辅助 API](#6-运维辅助-api) -7. [内部消息](#7-内部消息) -8. [公共参数](#8-公共参数) -9. [统一错误码](#9-统一错误码) - ---- - -## 1. API 总览 - -| # | API | 方向 | 权限 | 说明 | -|---|-----|------|------|------| -| 1 | `APIScanVmInstanceMetadataMsg` | 外部 | admin | 扫描主存储,返回有元数据的 VM 列表 | -| 2 | `APIReadVmInstanceMetadataMsg` | 外部 | admin | 读取指定 VM 的元数据 JSON | -| 3 | `APIRegisterVmInstanceFromMetadataMsg` | 外部 | admin | 从元数据注册 VM | -| 4 | `APICheckVmInstanceMetadataConsistencyMsg` | 外部 | admin | 检查 DB 与存储上元数据的一致性 | -| 5 | `APIUpdateVmMetadataMsg` | 外部 | admin | 手动触发指定 VM 的元数据全量刷写 | -| 6 | `APIPreCheckVmMetadataRegistrationMsg` | 外部 | admin | 注册前预检查 | -| 7 | `APICleanupVmInstanceMetadataMsg` | 外部 | admin | 批量清理指定范围的元数据文件/LV(仅 `enabled=false` 时可用) | -| 8 | `UpdateVmInstanceMetadataMsg` | 内部 | — | Poller/triggerFlush 发送给 VmInstanceBase | -| 9 | `UpdateVmInstanceMetadataOnPrimaryStorageMsg` | 内部 | — | 发送给主存储 handler | -| 10 | `UpdateVmInstanceMetadataOnHypervisorMsg` | 内部 | — | 发送给 Host Agent | - ---- - -## 2. 扫描虚拟机元数据 - -### 2.1 APIScanVmInstanceMetadataMsg - -扫描指定主存储上的元数据,返回有元数据文件/LV 的 VM 列表及摘要信息。 - -> **命名理由**:使用 `Scan` 而非 `Get`,因为该 API 不是从 DB 查询,而是触发 Agent 扫描存储,是一次性 I/O 操作。`Scan` 语义更准确,避免与标准 `APIGet*` 查询模式混淆。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| primaryStorageUuids | List\ | 否 | 指定主存储 UUID 列表;为空则扫描所有已连接 PS | -| vmUuids | List\ | 否 | 仅扫描指定 VM 的元数据;为空则扫描全部 | - -**响应 — APIScanVmInstanceMetadataReply** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| metadataList | List\ | 扫描结果列表 | - -**VmMetadataScanResult** - -| 字段 | 类型 | 说明 | -|------|------|------| -| vmUuid | String | VM UUID | -| vmName | String | VM 名称(来自 sblk Header 摘要 / JSON 文件内容) | -| vmCategory | String | VM 类别(REGULAR / TEMPLATE / TEMPLATE_CACHE) | -| primaryStorageUuid | String | 元数据所在主存储 UUID | -| primaryStorageType | String | 主存储类型(SharedBlock / LocalStorage / NFS) | -| schemaVersion | String | 元数据 schema 版本 | -| lastUpdateTime | Long | 最后更新时间戳(epoch ms) | -| metadataPath | String | 元数据路径(sblk LV path / JSON file path) | -| sizeBytes | Long | 元数据占用空间(字节) | - -### 2.2 实现说明 - -- sblk:调用 Agent 扫描 VG 中所有 `*_vmmeta` LV,读取 Header 提取摘要信息(见 Part 4e §1) -- local/NFS:扫描 `{mountPath}/.zstack-vm-metadata/` 目录下 JSON 文件 -- 扫描结果不含元数据内容,仅含摘要(轻量级) -- 对应的 Java 文件为 `APIScanVmInstanceMetadataMsg.java` / `APIScanVmInstanceMetadataReply.java`,位于 `header/storage/primary/` - -> v2+ 规划(Q5-1):Scan API 将补充分页参数 `start`/`limit`(或 `offset`/`limit`),避免大规模环境一次性返回过大结果集。 - ---- - -## 3. 读取虚拟机元数据 - -### 3.1 APIReadVmInstanceMetadataMsg - -读取指定 VM 的完整元数据 JSON 内容。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| vmUuid | String | 是 | 要读取的 VM UUID | -| primaryStorageUuid | String | 是 | 元数据所在主存储 UUID | - -> **空 payload 处理**:若读取到的 payload 为空 JSON `{}`(初始化后尚未写入完整数据),仍返回 `readStatus=OK` + `metadataContent="{}"`。调用方检查 payload 内容有无实质字段决定是否可注册。 -> -> **`__readStatus` 嵌入**:Read API 在返回 `metadataContent` 时,将当前 `readStatus` 值以 `"__readStatus": ""` 字段嵌入 JSON 根级别。此字段供 Register API 入口校验数据可用性(见 [Part 3 §3.3-1](vm-metadata-03-注册与运维.md#33-完整注册步骤))。手动构造的 metadataContent 若不含此字段,Register 视为 OK 继续。 - -**响应 — APIReadVmInstanceMetadataReply** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| metadataContent | String | 完整元数据 JSON 字符串 | -| schemaVersion | String | 元数据 schema 版本 | -| readStatus | String | OK / NEED_REPAIR / RECOVERED / DEGRADED / STORAGE_CHANGE_INCOMPLETE / CORRUPTED | -| repairAction | String | 可为 null。NEED_REPAIR/RECOVERED 时提示的修复动作(如 "complete_phase3" / "rebuild_header" / "full_refresh") | -| warnings | List\ | 读取过程中的非致命警告 | - -### 3.2 readStatus 说明 - -| 状态 | 含义 | payload | is_usable | 后续操作 | -|------|------|---------|-----------|----------| -| OK | 正常读取,Checksum 校验通过 | (Y) 有效 | (Y) | — | -| NEED_REPAIR | Slot 可读但 Header 需修复(sblk,PendingOp 残留) | (Y) 有效 | (Y) | 管理平面发送 `RepairMetadataMsg` | -| RECOVERED | Header 损坏但通过 Slot 自描述恢复成功 | (Y) 有效 | (Y) | 管理平面发送 `RepairMetadataMsg` 重建 Header | -| STORAGE_CHANGE_INCOMPLETE | 存储拓扑已变更但元数据未更新(PendingOp=2 且 Phase 2 未完成) | (!) stale | (N) | **禁止注册**,必须 `markDirty` 全量重写 | -| DEGRADED | 单 Slot 损坏,通过另一 Slot 降级读取成功 | (!) 有效(非最新) | (Y) | 允许注册(如灾备场景)+ 必须告警 + 触发修复 | -| CORRUPTED | A/B 双 Slot 均损坏(sblk)或文件内容无效 | (N) | (N) | `markDirty` 全量重写 | - -> sblk 读取与恢复的完整流程见 [Part 4d](vm-metadata-04d-sblk读取与恢复.md)。 -> -> v2+ 规划(Q5-2):Read API 增加 streaming/分块返回模式,降低超大 payload 场景下单次响应体压力。 - ---- - -## 4. 注册虚拟机 - -### 4.1 APIRegisterVmInstanceFromMetadataMsg - -从元数据注册虚拟机。详细注册流程见 [Part 3](vm-metadata-03-注册与运维.md)。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| metadataContent | String | 是 | 完整元数据 JSON(通常来自 `APIReadVmInstanceMetadataMsg` 的响应)。大小限制:超过 30MB 拒绝 | -| targetPrimaryStorageUuid | String | 是 | 目标主存储 UUID | -| zoneUuid | String | 是 | 目标 Zone UUID | -| clusterUuid | String | 是 | 目标 Cluster UUID | -| forceVersionMismatch | Boolean | 否 | 默认 false。设为 true 时允许 schemaVersion 不匹配的强制注册 | - -**响应 — APIRegisterVmInstanceFromMetadataEvent** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| inventory | VmInstanceInventory | 注册成功的 VM Inventory | -| warnings | List\ | 注册过程中的非致命警告(如 imageUuid 不存在、diskOfferingUuid 已清空、模板 VM 降级为普通 VM 等) | - -> **LongJob**:注册操作通过 `LongJob` 框架异步执行,超时时间默认 30 分钟。原因:注册涉及大量 DB 写入 + Agent 调用(快照链变基),耗时可能较长。LongJob 提供进度查询、超时保护和 API 线程释放。 -> -> **输入校验**:`metadataContent` 大小超过 30MB 立即拒绝(与 Part 2b §10 的 payload 大小保护一致)。校验在 API 入口层执行,在 JSON 解析之前。 - -### 4.2 状态转换 - -``` -(new) → Registering → Stopped - │ - └── 失败 → 回滚删除所有 VO -``` - -注册成功后 VM 处于 `Stopped` 状态。用户需要先添加网卡(`AttachVmNicToVm`)再启动。 - -### 4.3 注册后首次启动 - -VM 首次从 Stopped 转为 Running 时,删除 `vm.metadata.registered.not.started` ResourceConfig,立即触发 `markDirty`。此后元数据正常跟踪(见 [Part 3 §3.2](vm-metadata-03-注册与运维.md#32-注册-vm-未首次启动-resourceconfig))。 - -### 4.4 v2+ 规划:批量注册 API - -> 当前注册 N 个 VM 需执行 4N 次 API 调用(Scan + Read + PreCheck + Register × N),大规模灾备恢复场景(100+ VM)效率较低。 -> -> v2+ 计划引入 `APIBatchRegisterVmInstanceFromMetadataMsg`: -> - **输入**:`vmUuids`(List\)+ `primaryStorageUuid` + `zoneUuid` + `clusterUuid`。内部自动执行 Read + PreCheck + Register。 -> - **并行策略**:按 `vm.metadata.global.maxConcurrent` 控制并行度,分批注册。 -> - **部分成功**:返回 `List`,每个 VM 独立成功/失败,不因单 VM 失败中止整批。 -> - **进度查询**:通过 LongJob 框架提供进度百分比(已完成 / 总数)。 - ---- - -## 5. 检查虚拟机元数据一致性 - -### 5.1 APICheckVmInstanceMetadataConsistencyMsg - -从 DB 构建当前元数据 → 从存储读取已持久化元数据 → 结构化比较。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| vmUuids | List\ | 否 | 指定 VM 列表;为空则检查所有已启用元数据的 VM | -| primaryStorageUuid | String | 否 | 限定主存储范围 | -| autoRepair | Boolean | 否 | 默认 `false`。`true` 时对可修复不一致项自动执行 `markDirty(vmUuid)`(v1.1 新增) | - -**响应 — APICheckVmInstanceMetadataConsistencyReply** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| results | List\ | 每个 VM 的检查结果 | - -**ConsistencyCheckResult** - -| 字段 | 类型 | 说明 | -|------|------|------| -| vmUuid | String | VM UUID | -| consistent | Boolean | 是否一致 | -| diffs | List\ | 不一致的项目列表 | -| action | String | 自动修复操作(NONE / MARK_DIRTY) | - -### 5.2 比较排除字段 - -以下字段在比较时忽略(属于运行时变化字段): - -- `lastOpDate` — 时间戳字段 -- `id` — 自增 ID(SystemTag、ResourceConfig) -- `managementNodeUuid` — 运行时绑定 MN - -> **Q37 — 排除字段完整清单**:除上述 3 项外,以下字段也需排除: -> - `accountUuid` — 注册场景中会被替换为目标环境 accountUuid,不应参与比对 -> - `createDate` — 新创建 VO 的 createDate 与元数据中记录的不同(每次 persist 时由 DB 生成) -> - `VmInstanceVO.hostUuid` — 运行时动态绑定,VM Stopped 时为 null -> - `VmInstanceVO.lastHostUuid` — 运行时动态绑定 -> - `VmInstanceVO.state` — 运行时状态,不属于结构化配置 -> - `VolumeVO.actualSize` — Agent 端物理大小,不参与元数据一致性判定 -> - `VolumeVO.status` — 运行时状态(Ready/NotInstantiated) -> -> **比对逻辑**:先按 `VmInstanceVO.uuid` 匹配 VM 主记录,再按各子 VO 的 `uuid` 逐项匹配 Volume/Snapshot/SystemTag/ResourceConfig。匹配成功后逐字段比对(排除上述字段)。排除字段列表允许通过 `ConsistencyCheckExcludedFields` 静态常量扩展,新增字段时添加注释说明排除原因。 - -### 5.3 自动修复 - -发现不一致时,自动调用 `markDirty(vmUuid)` 触发全量重写。这是内部消息丢失 `markDirty()` 的批量补救手段(见 [Part 2b §12.4 D1 补充说明](vm-metadata-02b-高可用与运维.md#d1-补充说明--内部消息-handler-遗漏-markdirty-的补救))。 - -> 行为约束(Q5-3):仅当 `autoRepair=true` 时执行自动修复;默认 `false` 只返回检查结果与建议动作,避免检查 API 带来隐式写入副作用。 - -**自动修复边界表**: - -| 场景 | DB 状态 | 存储元数据状态 | 修复动作 | 说明 | -|------|---------|--------------|----------|------| -| DB 比存储新 | 有字段差异 | 旧版本 | MARK_DIRTY | 正常情况,刷写延迟或 markDirty 遗漏 | -| DB 缺少 UUID | VM 存在 | 存储上有元数据但 UUID 未在 DB 中 | MANUAL_CHECK | 可能是孤儿元数据,需人工确认 | -| 存储元数据损坏 | VM 存在 | CORRUPTED / 无法解析 | MARK_DIRTY | 全量重建 | -| 存储不可达 | VM 存在 | 无法读取 | SKIP + WARN | 不执行修复,记录告警 | - ---- - -## 6. 运维辅助 API - -### 6.1 手动触发元数据更新 - -#### APIUpdateVmMetadataMsg - -指定 vmUuid,手动触发一次全量元数据更新。适用于运维人员发现元数据滞后时的即时修复。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| vmUuid | String | 是 | 目标 VM UUID | - -**响应 — APIUpdateVmMetadataEvent** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | - -**实现**:直接调用 `markDirty(vmUuid, true)` 标脏为 STORAGE 级别(全量),triggerFlush 立即处理。 - -> 并发说明(Q5-4):`APIUpdateVmMetadataMsg` 的同 VM 并发更新由 `ChainTask "update-vm-{vmUuid}-metadata"` 串行化保证,无需额外 API 级锁。 - -### 6.2 注册预检查 - -#### APIPreCheckVmMetadataRegistrationMsg - -在正式注册前执行预检查,返回所有检查项的通过/失败状态,帮助用户提前发现问题。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| metadataContent | String | 是 | 完整元数据 JSON | -| targetPrimaryStorageUuid | String | 是 | 目标主存储 UUID | -| zoneUuid | String | 否 | 目标 Zone UUID。若提供,额外校验 Zone 存在性及与 PS 的归属关系 | -| clusterUuid | String | 否 | 目标 Cluster UUID。若提供,额外校验 Cluster 存在性、与 PS 的连接性、及 Zone/Cluster 归属一致性 | -| forceVersionMismatch | Boolean | 否 | 默认 false。设为 true 时 `SCHEMA_VERSION_MATCH` 检查项不阻塞 | - -**响应 — APIPreCheckVmMetadataRegistrationReply** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| checkResults | List\ | 各检查项结果 | - -**PreCheckItem** - -| 字段 | 类型 | 说明 | -|------|------|------| -| name | String | 检查项名称 | -| passed | Boolean | 是否通过 | -| message | String | 检查详情 / 失败原因 | - -### 6.3 清理虚拟机元数据 - -#### APICleanupVmInstanceMetadataMsg - -批量清理指定范围的虚拟机元数据文件/LV 及关联 DB 记录。仅在 `vm.metadata.enabled=false` 时允许执行。 - -> **使用场景**:运维在关闭元数据功能后(`true → false`),按需回收存储空间。系统不自动清理,避免误操作丢失容灾数据。 - -**前置约束**:`vm.metadata.enabled` 必须为 `false`,否则返回错误 `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`。 - -**请求参数** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| primaryStorageUuids | List\ | 否 | 指定主存储范围。为空则清理所有 PS | -| vmUuids | List\ | 否 | 指定 VM 范围。为空则清理所有 VM | - -> 两个参数均为空时,清理**全部已存在元数据的 VM**。两个参数同时提供时取交集。 - -**响应 — APICleanupVmInstanceMetadataEvent** - -| 字段 | 类型 | 说明 | -|------|------|------| -| success | Boolean | — | -| totalCleaned | Integer | 成功清理的 VM 数量 | -| totalFailed | Integer | 清理失败的 VM 数量 | -| failedVmUuids | List\ | 清理失败的 VM UUID 列表(便于重试) | - -**实现流程**: - -1. 前置检查:`vm.metadata.enabled == false` -2. 根据参数确定清理范围(查 `VmMetadataPathFingerprintVO` 获取有元数据的 VM 列表) -3. 分批执行(keyset 分页,批次大小复用 `vm.metadata.upgrade.refreshBatchSize`): - - 对每个 VM 调用 `metadataStorageHandler.deleteMetadata(psUuid, vmUuid)` - - 删除 `VmMetadataPathFingerprintVO` 记录 - - 删除残留 `VmMetadataDirtyVO` 记录(`INSERT IGNORE` 插入后未消费的行) -4. 汇总结果,部分失败不中止(best-effort),返回失败列表供运维重试 - -**幂等性**:`deleteMetadata` 遵循 C-01C-9 约束(删除不存在的元数据视为成功),重复调用不报错。 - -**并发控制**:使用全局 ChainTask `"cleanup-vm-metadata-global"`(syncLevel=5)限流,避免对存储造成批量删除压力。 - -### 6.4 预检查项清单 - -| 检查项 | 说明 | 失败级别 | -|--------|------|----------| -| `FORMAT_VALID` | 元数据 JSON 格式、Base64 编码完整性 | BLOCK | -| `SCHEMA_VERSION_MATCH` | `schemaVersion == dbf.getDbVersion()`(精确匹配) | BLOCK(除非 `forceVersionMismatch=true`) | -| `VM_CATEGORY_CHECK` | vmCategory 不是 TEMPLATE_CACHE | BLOCK | -| `UUID_CONFLICT` | VM/Volume/Snapshot 等 UUID 无冲突 | BLOCK | -| `PS_REACHABLE` | 目标主存储可达且状态正常 | BLOCK | -| `PS_TYPE_SUPPORTED` | 主存储类型支持元数据(sblk/local/NFS) | BLOCK | -| `CROSS_STORAGE_CHECK` | 所有磁盘属于同一主存储 | BLOCK | -| `INSTALL_PATH_EXIST` | 替换后路径在存储上存在。Root Volume 缺失为 BLOCK,Data Volume 缺失为 WARN | Root=BLOCK / Data=WARN | -| `READ_STATUS_USABLE` | 元数据 `__readStatus` 不为 CORRUPTED 或 STORAGE_CHANGE_INCOMPLETE | BLOCK | -| `CDROM_DETECTED` | 检测到 VM 挂载了 CDROM / ISO,注册后可能不可用 | WARN | - -> **schemaVersion 校验逻辑**:使用精确匹配 `==` 比较数据库版本。不支持低版本数据库注册高版本元数据,也不支持高版本数据库注册低版本元数据(除非 `forceVersionMismatch=true`)。参见 [Part 1a §6.2](vm-metadata-01a-数据模型与序列化.md#62-注册时校验规则)。 - -`INSTALL_PATH_EXIST` 检查实现示例(Q5-6): - -```java -if (volume.isRootVolume() && !pathExists(volume.getInstallPath())) { - result.add(PreCheckItem.block(INSTALL_PATH_EXIST, ...)); -} else if (!pathExists(volume.getInstallPath())) { - result.add(PreCheckItem.warn(INSTALL_PATH_EXIST, ...)); -} -``` - -> `CDROM_DETECTED` 处理说明(Q5-7):CDROM/ISO 挂载信息不在 VM 元数据范围内,注册后如业务需要须手动重新挂载。 - ---- - -## 7. 内部消息 - -### 7.1 UpdateVmInstanceMetadataMsg - -由 Poller/triggerFlush 发送给 `VmInstanceBase`,触发构建元数据并写入主存储。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| uuid | String | VM UUID | -| storageStructureChange | Boolean | 是否涉及存储拓扑变更(OP type 标记) | - -路由:`makeLocalServiceId(msg, VmInstanceConstant.SERVICE_ID)` - -超时:`setTimeout(5min)` — 大 payload O_DIRECT 写入 + 可能的 lvextend + 构建耗时(与 Part 2 §5.1 一致) - -### 7.2 UpdateVmInstanceMetadataOnPrimaryStorageMsg - -由 VmInstanceBase 发送给主存储 handler。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| vmUuid | String | VM UUID | -| payload | String | 序列化后的元数据 JSON | -| storageStructureChange | Boolean | OP type | - -路由:`makeLocalServiceId` - -### 7.3 UpdateVmInstanceMetadataOnHypervisorMsg - -由主存储 handler 发送给 Host Agent。 - -> **Agent 通信安全**:HTTP 请求携带 `agentToken`(通过 `X-ZStack-Agent-Token` header 传递),Agent 端校验 token 一致性。这与 ZStack 其他 Agent 通信一致,无额外认证机制。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| hostUuid | String | 目标主机 UUID | -| vmUuid | String | VM UUID | -| payload | String | 元数据 JSON | -| installPath | String | 元数据存储路径 | -| storageStructureChange | Boolean | OP type | - -路由:`makeTargetServiceIdByResourceUuid(hostUuid)` — hash 环路由到 host-owner MN - -超时:`setTimeout(2min)` - -### 7.4 消息调用链 - -``` -VmMetadataUpdateInterceptor / Poller - → markDirty + triggerFlushForVm - → ChainTask "update-vm-metadata-global" - → ChainTask "update-vm-{vmUuid}-metadata" - → bus.send(UpdateVmInstanceMetadataMsg) - → VmInstanceBase: build payload - → bus.send(UpdateVmInstanceMetadataOnPrimaryStorageMsg) - → PS handler: ChainTask "update-metadata-on-ps-{psUuid}" - → bus.send(UpdateVmInstanceMetadataOnHypervisorMsg) - → HostBase → HTTP call to Agent -``` - -完整消息链描述见 [Part 2 §5](vm-metadata-02-脏标记与Poller.md#5-消息调用链)。 - -### 7.5 RepairMetadataMsg - -由管理平面发送给主存储 handler,用于修复 sblk Header(包括完成未完成的 Phase 3、清除 PendingOp、重建 Header)。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| vmUuid | String | VM UUID | -| primaryStorageUuid | String | 元数据所在主存储 UUID | -| repairAction | String | 修复动作:`complete_phase3` / `clear_pending_op` / `rebuild_header` / `full_refresh` | - -路由:`makeLocalServiceId` → 主存储 handler → Agent HTTP 调用 - -> `full_refresh` 等价于 `markDirty(vmUuid, true)`,但通过显式消息而非 Poller 间接触发,便于日志追踪。 - -### 7.6 BatchCheckMetadataStatusMsg - -由管理平面发送给主存储 handler,批量检查多个 VM 的元数据 Header 状态,用于健康巡检。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| primaryStorageUuid | String | 目标主存储 UUID | -| vmUuids | List\ | 要检查的 VM UUID 列表 | - -**响应 — BatchCheckMetadataStatusReply** - -| 字段 | 类型 | 说明 | -|------|------|------| -| results | Map\ | key=vmUuid, value=状态结果 | - -**MetadataStatusResult** - -| 字段 | 类型 | 说明 | -|------|------|------| -| readStatus | String | OK / NEED_REPAIR / RECOVERED / DEGRADED / STORAGE_CHANGE_INCOMPLETE / CORRUPTED | -| repairAction | String | 可为 null | -| lastUpdateTime | Long | 最后更新时间戳 | -| pendingOp | Integer | 当前 PendingOp 值(0/1/2) | - -路由:`makeLocalServiceId` → 主存储 handler → Agent HTTP 调用(批量读 Header,不读 Slot) - ---- - -## 8. 公共参数 - -### 8.1 GlobalConfig - -| 配置项 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `vm.metadata.enabled` | Boolean | false | 元数据功能总开关 | -| `vm.metadata.dirty.pollIntervalSec` | Long | 5 | Poller 轮询间隔(秒) | -| `vm.metadata.dirty.batchSize` | Integer | 50 | 每轮 Poller 最多认领行数 | -| `vm.metadata.maxRetry` | Integer | 5 | 最大重试次数 | -| `vm.metadata.ps.maxConcurrent` | Integer | 5 | 同一 MN 同一 PS 最大并发写入 | -| `vm.metadata.global.maxConcurrent` | Integer | 10 | 同一 MN 最大并发 VM 更新数 | -| `vm.metadata.pathCheck.intervalSec` | Long | 300 | 路径指纹巡检间隔(秒) | - -> 完整 GlobalConfig 配置说明见 [Part 2b §13](vm-metadata-02b-高可用与运维.md#13-globalconfig-配置项汇总)(权威来源)。本表仅为快速参考。 - -### 8.2 权限约束 - -所有 API 仅限 **admin** 操作。注册 VM 场景为灾难恢复,不面向普通用户。 - ---- - -## 9. 统一错误码 - -> **权威来源**:所有与 VM 元数据相关的错误码在此统一定义。其他文档应引用本节。 - -| 错误码 | 适用 API | 说明 | -|--------|---------|------| -| `METADATA_INVALID_FORMAT` | Read / Register / PreCheck | 元数据 JSON 格式错误、Base64 解码失败或校验器不通过 | -| `METADATA_SCHEMA_VERSION_MISMATCH` | Register / PreCheck | `schemaVersion != dbf.getDbVersion()`,且未设置 `forceVersionMismatch=true` | -| `METADATA_UUID_CONFLICT` | Register / PreCheck | VM、Volume、Snapshot 等 UUID 与已有资源冲突 | -| `METADATA_STORAGE_NOT_SUPPORTED` | Register / PreCheck / Scan | 主存储类型不支持元数据功能(如 Ceph、ZBS、vhost) | -| `METADATA_CROSS_STORAGE_FORBIDDEN` | Register / PreCheck | 元数据中的磁盘分布在多个主存储上 | -| `METADATA_INSTALL_PATH_NOT_FOUND` | Register | 替换后的 installPath 在目标存储上不存在 | -| `METADATA_CACHE_VM_NOT_REGISTERABLE` | Register / PreCheck | vmCategory = TEMPLATE_CACHE,缓存 VM 拒绝注册 | -| `METADATA_VM_REGISTERING` | Register | 目标 VM 正在被另一个注册操作处理中 | -| `METADATA_READ_CORRUPTED` | Read | A/B 双 Slot 均损坏(sblk)或文件不可读 | -| `METADATA_PAYLOAD_TOO_LARGE` | Update(内部) | Payload 超过 30MB 上限 | -| `METADATA_PS_UNREACHABLE` | PreCheck / Register / Update | 目标主存储不可达或状态异常 | -| `METADATA_FEATURE_DISABLED` | All | `vm.metadata.enabled = false` 时调用 API | - -### 9.1 错误码格式 - -所有错误码使用 `SysErrors.METADATA_` 前缀,在 `VmMetadataErrors` 枚举中统一定义。API Reply/Event 中通过标准 `ErrorCode` 结构返回。 diff --git "a/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" "b/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" deleted file mode 100644 index 25712a15d44..00000000000 --- "a/docs/design/vm-metadata-07a-\345\215\225\345\205\203\346\265\213\350\257\225\350\256\241\345\210\222.md" +++ /dev/null @@ -1,278 +0,0 @@ -# VM 元数据 — 单元测试计划 - -> 本文档为 VM 元数据功能的单元测试计划。单元测试聚焦**纯 Java 逻辑**,不依赖 DB/Agent/存储,通过 mock 隔离外部依赖。 -> 集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 - -## 目录 - -1. [元数据序列化 Round-Trip](#1-元数据序列化-round-trip) -2. [DTO 字段完整性](#2-dto-字段完整性) -3. [路径指纹与漂移检测](#3-路径指纹与漂移检测) -4. [markDirty 逻辑](#4-markdirty-逻辑) -5. [MetadataImpact 注解覆盖率](#5-metadataimpact-注解覆盖率) -6. [VM UUID Resolver 链](#6-vm-uuid-resolver-链) -7. [Payload 容量计算](#7-payload-容量计算) -8. [sblk 二进制布局编解码](#8-sblk-二进制布局编解码) -9. [注册字段映射](#9-注册字段映射) -10. [installPath 前缀替换](#10-installpath-前缀替换) - ---- - -## 1. 元数据序列化 Round-Trip - -**覆盖约束**:Part 1a §2–§3 - -### 1.1 基础 Round-Trip - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-SER-01 | 最小 VM(单根盘、无快照、无数据盘) | 构造最小 `VmInstanceMetadataDTO` | Gson 序列化 → 反序列化 → 与原对象 `equals` | -| UT-SER-02 | 完整 VM(根盘 + 3 数据盘 + 快照链 + NIC + SystemTag + ResourceConfig) | 构造满字段 DTO | Round-Trip 后所有字段一致 | -| UT-SER-03 | 含 null 字段的 VM(imageUuid=null, instanceOfferingUuid=null) | DTO 部分字段为 null | Gson 序列化跳过 null 字段(`serializeNulls=false`),反序列化后 null 字段仍为 null | -| UT-SER-04 | 空快照列表 | `snapshots = Collections.emptyList()` | 序列化为 `"snapshots":[]`,反序列化后 `.size()==0` 且非 null | -| UT-SER-05 | 深度快照链(256 层嵌套 parentUuid) | 构造 depth=256 的链式快照 | Round-Trip 后 parentUuid 链完整保留 | - -### 1.2 编码管线 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-SER-10 | SystemTag Base64 编码 Round-Trip | `List` → JSON → Base64 → DTO.systemTags (String) | 解码后还原为等价 `List` | -| UT-SER-11 | ResourceConfig Base64 编码 Round-Trip | 同上 | 解码后还原 | -| UT-SER-12 | 空 SystemTag 列表 | `systemTags = []` → Base64 | 编码非空字符串,解码还原为空列表 | -| UT-SER-13 | 含特殊字符的 tag(中文、emoji、`=` 分隔符) | `tag::key::中文值(fire)` | Round-Trip 后完整保留 | - -### 1.3 JSON 确定性 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-SER-20 | 同一 DTO 多次序列化字节一致 | 同一对象序列化 10 次 | 10 次 `byte[]` 完全相同 | -| UT-SER-21 | 字段声明顺序稳定性 | 检查 Gson 输出中 `uuid` 在 `name` 前(按声明顺序) | JSON key 顺序与 Java 字段声明顺序一致 | -| UT-SER-22 | `@SerializedName` 注解生效 | DTO 中带 `@SerializedName("vm_uuid")` 的字段 | JSON key 为 `vm_uuid` 而非 Java 字段名 | - -### 1.4 版本兼容 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-SER-30 | 反序列化缺失字段(旧版本数据) | 不含新版字段 `vmCategory` 的 JSON | 反序列化成功,`vmCategory==null` | -| UT-SER-31 | 反序列化多余字段(新版本数据) | JSON 含当前 DTO 无对应的 `futureField` | Gson 默认忽略未知字段,不报错 | -| UT-SER-32 | schemaVersion 精确匹配检查 | `expected=3, actual=2` | `isVersionMatch()` 返回 false | - ---- - -## 2. DTO 字段完整性 - -**覆盖约束**:Part 1a §1, §4 - -### 2.1 快照树构建 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-DTO-01 | 空快照列表构建树 | `List = []` | 返回空树列表 | -| UT-DTO-02 | 单棵树、线性链(A→B→C) | 3 个 VO,parentUuid 链式指向 | 树结构正确:root=A, A.children=[B], B.children=[C] | -| UT-DTO-03 | 多棵独立树 | 2 棵树各 3 个节点,volumeSnapshotTreeUuid 不同 | 返回 2 棵独立树 | -| UT-DTO-04 | 分叉树(A→B, A→C) | 3 个 VO,B 和 C 的 parentUuid 都指向 A | A.children 包含 B 和 C | -| UT-DTO-05 | 共享磁盘快照排除 | 快照列表含 `VolumeVO.isShareable=true` 的卷快照 | 构建时跳过该卷的快照 | - -### 2.2 VolumeSnapshotReferenceVO 查询范围 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-DTO-10 | 按 referenceVolumeUuid 查询(非 volumeUuid) | mock DB 返回:ref.referenceVolumeUuid 匹配当前 VM 卷 | 仅返回当前 VM 的引用记录,不含父模板的引用 | -| UT-DTO-11 | VM 无引用记录 | `referenceVolumeUuid` 无匹配 | 返回空列表 | - -### 2.3 SystemTag/ResourceConfig 白名单过滤 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-DTO-20 | 白名单内 tag 保留 | `bootMode::UEFI` | 保留在构建结果中 | -| UT-DTO-21 | 白名单外 tag 过滤 | `ephemeral::true`(假设不在白名单) | 不在构建结果中 | -| UT-DTO-22 | ResourceConfig 按类型分组 | VM 级 + Volume 级 config | 分别归入 `vmConfigs` 和 `volumeConfigs` | - ---- - -## 3. 路径指纹与漂移检测 - -**覆盖约束**:Part 2b §8.2 - -### 3.1 路径快照构建 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-FP-01 | 正常路径快照 JSON | 2 个 Volume + 3 个 Snapshot(有序) | JSON 中 volumes/snapshots 按 uuid ASC 排列 | -| UT-FP-02 | 相同拓扑的确定性 | 两次构建(不同对象实例,相同内容) | JSON 字符串 `byte[]` 完全相同 | -| UT-FP-03 | 空快照列表 | 仅有 Volume 无 Snapshot | `"snapshots":[]`,不影响比对 | - -### 3.2 路径漂移检测 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-FP-10 | 无漂移 | recorded == current | 不触发 markDirty | -| UT-FP-11 | Volume installPath 变更 | current 中 vol-aaa 路径不同 | 检测到 drift → 调用 markDirty | -| UT-FP-12 | Snapshot 新增 | current 中多一个 snapshot | 检测到 drift | -| UT-FP-13 | Snapshot 删除 | current 中少一个 snapshot | 检测到 drift | - ---- - -## 4. markDirty 逻辑 - -**覆盖约束**:C-DM-01, C-SC-07, C-FL-08 - -### 4.1 标脏语义 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-MD-01 | 首次 markDirty | vmUuid 不存在 dirty 行 | INSERT IGNORE 创建新行,`dirtyVersion=1` | -| UT-MD-02 | 重复 markDirty | vmUuid 已有 dirty 行 | UPDATE `dirtyVersion+1`,行唯一 | -| UT-MD-03 | storageStructureChange OR 语义 | 先 markDirty(false) 再 markDirty(true) | dirty 行 `storageStructureChange=true`(不会被覆盖回 false) | -| UT-MD-04 | storageStructureChange 反向不降级 | 先 markDirty(true) 再 markDirty(false) | 仍为 `storageStructureChange=true` | -| UT-MD-05 | vm.metadata.enabled=false 时 markDirty | 开关关闭 | markDirty 直接 return,不创建 dirty 行 | -| UT-MD-06 | Destroyed VM 不标脏 | vmState=Destroyed | markDirty 直接 return(C-FL-08 前置过滤) | - -### 4.2 retryCount 与退避 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-MD-10 | 新 markDirty 的 retryCount | 首次创建 | `retryCount=0, nextRetryTime=NULL` | -| UT-MD-11 | markDirty 不重置已有 retryCount | dirty 行 retryCount=3 时再次 markDirty | retryCount 不变(仅递增 dirtyVersion) | -| UT-MD-12 | 指数退避计算 | baseDelay=10s, maxExponent=10, retryCount=3 | `nextRetryTime = now + 10 * 2^3 = 80s` | -| UT-MD-13 | 退避上限 | retryCount=15(超过 maxExponent=10) | `nextRetryTime = now + 10 * 2^10 = 10240s`(封顶) | - ---- - -## 5. MetadataImpact 注解覆盖率 - -**覆盖约束**:C-IM - -### 5.1 CI 扫描验证 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-AN-01 | 所有 APIMessage 子类有 @MetadataImpact | 反射扫描所有 `APIMessage` 子类 | 每个子类都标注了 `@MetadataImpact`(NONE/CONFIG/STORAGE) | -| UT-AN-02 | STORAGE 级 API 不误标为 CONFIG | 检查 APIDeleteVolumeSnapshotMsg 等 | 快照/迁移/删盘类 API 标注为 `Impact.STORAGE` | -| UT-AN-03 | 纯查询 API 标注 NONE | 检查 QueryVmInstanceMsg 等 | `Impact.NONE` | - -### 5.2 内部消息白名单 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-AN-10 | INTERNAL_METADATA_MESSAGES 包含所有 STORAGE 级内部消息 | 反射扫描 + 对比白名单 | 白名单完整 | -| UT-AN-11 | 白名单中每个消息的 handler 调用了 markDirty | 静态分析 / mock 验证 | 所有 handler 成功路径包含 markDirty 调用 | - ---- - -## 6. VM UUID Resolver 链 - -**覆盖约束**:C-RS, Part 1b §3 - -### 6.1 Resolver 选择 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-RS-01 | VmInstanceMessage 直接获取 vmUuid | APIStopVmInstanceMsg(vmUuid="vm-1") | Resolver 返回 `["vm-1"]` | -| UT-RS-02 | VolumeMessage 通过 Volume→VM 查询 | APIDeleteVolumeMsg(volumeUuid="vol-1") + mock vol-1.vmInstanceUuid="vm-1" | 返回 `["vm-1"]` | -| UT-RS-03 | TagMessage 按 resourceType 路由 | APICreateSystemTagMsg(resourceUuid="vol-1", resourceType="VolumeVO") | 返回 vol-1 关联的 VM UUID | -| UT-RS-04 | 无法解析的 API | 自定义 API 无匹配 Resolver | 返回空列表 + WARN 日志 | -| UT-RS-05 | 删除/卸载类 API 使用 pre-capture | APIDetachVolumeMsg | `resolveVmUuids()` 在 `beforeDeliveryMessage` 阶段调用(C-RS) | -| UT-RS-06 | 单个 API 关联多个 VM | 批量 Tag 操作涉及多个 VM | 返回所有关联 VM UUID(去重) | - ---- - -## 7. Payload 容量计算 - -**覆盖约束**:Part 2b §10.0, C-02B-5, C-02B-7 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-CAP-01 | 4MB LV 的 slotCapacity | `lvSize = 4*1024*1024` | `slotCapacity = ((4MB - 4096) / 2 / 4096) * 4096 = 2,093,056` | -| UT-CAP-02 | 64MB LV(上限)的 slotCapacity | `lvSize = 64*1024*1024` | `slotCapacity = 33,550,336` | -| UT-CAP-03 | payload 可用空间 | `slotCapacity - 36 (SlotHeader)` | 4MB LV → 2,093,020 bytes;64MB LV → 33,550,300 bytes | -| UT-CAP-04 | WARN 阈值判定 | payloadSize = 8MB + 1 | 触发 WARN | -| UT-CAP-05 | REJECT 阈值判定 | payloadSize = 30MB + 1 | 触发 ERROR + 拒绝写入 | -| UT-CAP-06 | 常量集中定义验证 | 反射检查 `VmMetadataConstants` 类 | HEADER_SIZE/SLOT_HEADER_SIZE/MAX_LV_SIZE 均为 static final | - ---- - -## 8. sblk 二进制布局编解码 - -**覆盖约束**:Part 4b - -### 8.1 Header 编解码 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-BIN-01 | Header 序列化 Round-Trip | 构造 Header(Magic, ActiveSlot=0, WriteSequence=1, PendingOp=0) | 序列化为 4096 bytes → 反序列化还原 | -| UT-BIN-02 | Magic 校验 | `0x5A534D54` | 读取 Header 校验通过 | -| UT-BIN-03 | Magic 错误 | `0xDEADBEEF` | 读取 Header 抛出 `InvalidHeaderException` | -| UT-BIN-04 | ControlChecksum 校验 | 正常 Header + SHA-256 | checksum 验证通过 | -| UT-BIN-05 | ControlChecksum 篡改 | 修改 Header 一个字节后不更新 checksum | checksum 验证失败 | - -### 8.2 Slot 编解码 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-BIN-10 | Slot Header 序列化 | SlotMagic + SeqNum + Offset + Capacity + PayloadLen | 序列化为 36 bytes | -| UT-BIN-11 | Payload 写入与读取 | 1KB payload → 写入 Slot → 读取 | payload 内容一致 | -| UT-BIN-12 | Payload 恰好填满 Slot | payloadLen == slotCapacity - 36 | 写入成功,无溢出 | -| UT-BIN-13 | Payload 超过 Slot 容量 | payloadLen > slotCapacity - 36 | 抛出 `PayloadTooLargeException` | - -### 8.3 VM 摘要区 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-BIN-20 | 摘要区序列化 | vmUuid(32) + vmName(256) + vmCategory(1) | 写入 [96:928) 区域 | -| UT-BIN-21 | vmName 超过 256 bytes | UTF-8 编码后 > 256 | 截断到 256 bytes 边界(不截断 UTF-8 多字节字符中间) | -| UT-BIN-22 | SummaryChecksum 验证 | 正常摘要区 | SHA-256 通过 | - -### 8.4 WriteSequence 边界 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-BIN-30 | 正常递增 | seq=100 → 写入 → seq=101 | WriteSequence +1 | -| UT-BIN-31 | Long.MAX_VALUE 溢出 | seq=Long.MAX_VALUE | 写入后 seq=Long.MIN_VALUE(Java long 自然溢出),比较逻辑使用 `Long.compareUnsigned` 或差值判断 | - ---- - -## 9. 注册字段映射 - -**覆盖约束**:Part 3 §1 - -### 9.1 VmInstanceVO 映射 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-REG-01 | uuid 保留 | 元数据 vmUuid="vm-aaa" | 注册后 VmInstanceVO.uuid="vm-aaa" | -| UT-REG-02 | hostUuid / lastHostUuid 置 null | 元数据含原始 hostUuid | 注册后两者均为 null | -| UT-REG-03 | state 硬编码 Registering→Stopped | — | 创建时 state=Registering,成功后改为 Stopped | -| UT-REG-04 | imageUuid 不存在时置 null | mock `dbf.findByUuid(imageUuid)` 返回 null | imageUuid=null + warnings 含提示信息 | -| UT-REG-05 | accountUuid 替换为当前调用者 | 元数据 accountUuid="old-admin" | 注册后 accountUuid = 当前 session 的 accountUuid | - -### 9.2 VolumeVO 映射 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-REG-10 | primaryStorageUuid 替换 | 元数据 psUuid="old-ps" + 目标 psUuid="new-ps" | VolumeVO.primaryStorageUuid="new-ps" | -| UT-REG-11 | diskOfferingUuid 置 null | 元数据含原始 diskOfferingUuid | 注册后为 null | - -### 9.3 快照 VO 映射 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-REG-20 | 快照 parentUuid 保留 | 链式快照 A→B→C | parentUuid 关系完整 | -| UT-REG-21 | SnapshotGroupVO accountUuid 替换 | 元数据含原始 accountUuid | 注册后替换为当前调用者 | -| UT-REG-22 | SnapshotGroupRefVO 路径替换 | volumeSnapshotInstallPath 含旧前缀 | 替换为新前缀 | -| UT-REG-23 | ReferenceVO parentId 统一置 null | 元数据含 parentId=5 | 注册后 parentId=null(C-03-1) | - ---- - -## 10. installPath 前缀替换 - -**覆盖约束**:C-03-3, Part 3 §3.4 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| UT-PATH-01 | sblk VG UUID 替换 | `/dev/123xxx/vol-aaa` → oldPrefix=`/dev/123xxx/`, newPrefix=`/dev/456xxx/` | `/dev/456xxx/vol-aaa` | -| UT-PATH-02 | NFS 挂载路径替换 | `/mnt/old-nfs/vm-data/vol-aaa` → `/mnt/new-nfs/vm-data/vol-aaa` | 替换成功 | -| UT-PATH-03 | 分隔符边界保护 | oldPrefix=`/dev/oldVg`(无尾 `/`) | 替换拒绝或自动补 `/`(C-03-3) | -| UT-PATH-04 | 子串误命中防护 | oldPrefix=`/dev/vg1/`, 路径=`/dev/vg12/vol` | 不匹配,不替换(`startsWith` 精确匹配) | -| UT-PATH-05 | installPath 不匹配 oldPrefix | 路径前缀与 oldPrefix 不一致 | 报错(明确提示哪个路径不匹配) | -| UT-PATH-06 | 批量路径替换一致性 | 10 个 Volume + 20 个 Snapshot 的 installPath | 全部按相同规则替换,无遗漏 | diff --git "a/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" "b/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" deleted file mode 100644 index d99abf43880..00000000000 --- "a/docs/design/vm-metadata-07b-\351\233\206\346\210\220\346\265\213\350\257\225\350\256\241\345\210\222.md" +++ /dev/null @@ -1,276 +0,0 @@ -# VM 元数据 — 集成测试计划 - -> 集成测试需真实 DB(H2 内存/MySQL)和模拟 Agent,验证跨模块协作正确性。 -> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 - -## 目录 - -1. [sblk 写入与读取](#1-sblk-写入与读取) -2. [local/NFS JSON 读写](#2-localnfs-json-读写) -3. [Poller 端到端流程](#3-poller-端到端流程) -4. [API 拦截器与 markDirty 联动](#4-api-拦截器与-markdirty-联动) -5. [存储迁移元数据链路](#5-存储迁移元数据链路) -6. [注册端到端流程](#6-注册端到端流程) -7. [路径指纹巡检端到端](#7-路径指纹巡检端到端) -8. [API 端到端](#8-api-端到端) - ---- - -## 1. sblk 写入与读取 - -**覆盖约束**:Part 4a–4e, C-01C-2 - -### 1.1 基础写入读取 - -| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | -|---------|------|----------|------|------| -| IT-SBLK-01 | 首次写入 + 读取 | 新建 4MB LV(mock Agent) | writeMetadata(vmUuid, payload) → readMetadata(vmUuid) | 读取内容与写入 payload 完全一致 | -| IT-SBLK-02 | 覆盖写入 A/B Slot 切换 | 已写入 v1 | writeMetadata(v2) → 验证 Header.ActiveSlot 切换 | ActiveSlot 从 0→1(或 1→0) | -| IT-SBLK-03 | 连续 3 次写入后读取 | 空 LV | 写 v1→v2→v3 → readMetadata | 读取到 v3;WriteSequence=3 | -| IT-SBLK-04 | LV 命名格式验证 | — | initializeMetadata(vmUuid) | LV name = `{vm_uuid}_vmmeta`,长度 ≤ 39 字符 | - -### 1.2 Payload 大小与自动扩容 - -| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | -|---------|------|----------|------|------| -| IT-SBLK-10 | 小 payload(1KB) | 4MB LV | 写入 → 读取 | 成功,LV 未扩容 | -| IT-SBLK-11 | payload 超过 4MB LV 容量 | 4MB LV | 写入 2.5MB payload | Agent 触发 `lvextend` → LV 变为 8MB → 写入成功 | -| IT-SBLK-12 | payload 达 30MB 阈值 | 64MB LV | 写入 30MB + 1 payload | 返回 `VM_METADATA_PAYLOAD_TOO_LARGE` 错误 | -| IT-SBLK-13 | 扩容后 Header 布局更新 | 4MB→8MB | 写入 → 读取 Header | SlotA/B Offset 和 Capacity 反映 8MB 布局 | - -### 1.3 op_type 与 storageStructureChange - -| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | -|---------|------|----------|------|------| -| IT-SBLK-20 | CONFIG_UPDATE 写入 | dirty 行 storageStructureChange=false | flush → 观察 Agent 调用 | Agent 收到 op_type=1 (CONFIG_UPDATE) | -| IT-SBLK-21 | STORAGE_CHANGE 写入 | dirty 行 storageStructureChange=true | flush → 观察 Agent 调用 | Agent 收到 op_type=2 (STORAGE_CHANGE) | - -### 1.4 deleteMetadata 幂等性 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-SBLK-30 | 删除存在的 LV | deleteMetadata(vmUuid) | Agent lvremove 成功 | -| IT-SBLK-31 | 删除不存在的 LV | deleteMetadata(vmUuid) | 不抛异常(C-01C-9) | -| IT-SBLK-32 | 双重删除 | delete → delete | 第二次幂等成功 | - ---- - -## 2. local/NFS JSON 读写 - -**覆盖约束**:Part 1c §1.2, C-01C-10 - -### 2.1 基础读写 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-JSON-01 | 首次写入 | writeMetadata(vmUuid, payload) | 创建 `{mountPath}/.zstack-vm-metadata/{vmUuid}.json` | -| IT-JSON-02 | 读取刚写入的文件 | 写入 → readMetadata | 读取内容 == 写入 payload | -| IT-JSON-03 | 覆盖写入 | 写入 v1 → 写入 v2 → 读取 | 读取到 v2 | -| IT-JSON-04 | 容器目录自动创建 | `.zstack-vm-metadata/` 目录不存在 | writeMetadata 自动 mkdir -p(Δ-4) | - -### 2.2 原子写入验证 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-JSON-10 | tmp+fsync+rename 原子性 | 写入过程中观察文件系统 | 先出现 `.sc.tmp` 文件 → rename 后仅有 `.json` | -| IT-JSON-11 | tmp 残留(Agent 重启前) | 手动创建 `.sc.tmp`,Agent 启动 | Agent 启动清理所有 `.sc.tmp` 文件(C-01C-10) | -| IT-JSON-12 | readMetadata 遇到 `.sc.tmp` | 只有 `.sc.tmp` 无 `.json` | 读取返回空/NOT_FOUND(不读 tmp) | - -### 2.3 deleteMetadata 幂等性 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-JSON-20 | 删除存在的 JSON | deleteMetadata → 检查文件 | 文件已删除 | -| IT-JSON-21 | 删除不存在的文件 | 文件本不存在 → deleteMetadata | 不抛异常 | - ---- - -## 3. Poller 端到端流程 - -**覆盖约束**:Part 2 §4, C-CL-02, C-TM-03, C-RB-04 - -### 3.1 正常 flush 链路 - -| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | -|---------|------|----------|------|------| -| IT-POL-01 | markDirty → Poller 认领 → flush → 成功删除 | 一个 UserVm | markDirty(vmUuid) → 等待 Poller 周期 | dirty 行被删除;存储有元数据;PathFingerprint 已记录 | -| IT-POL-02 | 多 VM 并发 flush | 5 个 VM 各有 dirty 行 | Poller 运行 | 5 个 VM 全部 flush 成功 | -| IT-POL-03 | Poller 无 dirty 行时空转 | 无 dirty 行 | Poller 运行 | SELECT 0 rows,正常返回 | -| IT-POL-04 | lastClaimTime 写入 | 认领一行 | 检查 DB | `lastClaimTime` 非 null(C-CL-02) | - -### 3.2 flush 失败与重试 - -| 用例 ID | 场景 | 前置条件 | 步骤 | 期望 | -|---------|------|----------|------|------| -| IT-POL-10 | Agent 超时 → 退避重试 | mock Agent 超时 | Poller 认领 → flush 超时 | dirty 行保留,retryCount+1,nextRetryTime 设置退避 | -| IT-POL-11 | 重试 5 次耗尽 | mock Agent 持续失败 | 5 轮 Poller | dirty 行删除 + PathFingerprint.lastFlushFailed=true(C-SR-05) | -| IT-POL-12 | StaleRecoveryTask 重入队 | lastFlushFailed=true | 等待 stale recovery 周期 | markDirty(retryCount=0) + lastFlushFailed=false | - -### 3.3 dirtyVersion 不匹配 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-POL-20 | flush 期间新 markDirty | flush 开始 → 另一线程 markDirty → flush 完成 | onFlushSuccess 检测 dirtyVersion 不匹配 → 释放认领(不删除 dirty 行) | -| IT-POL-21 | flush 后 dirtyVersion 匹配 | 正常 flush | dirty 行删除 | - -### 3.4 并发控制 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-POL-30 | per-VM ChainTask maxPending=1 | 对同一 VM 快速提交 3 次 flush | running=1, pending=1, 第 3 次 exceedMaxPendingCallback 释放 | -| IT-POL-31 | globalFlushInFlight 上限 | mock 11 个 VM flush 中(maxConcurrent=10) | 第 11 个 submitFlushTask 时 AtomicInteger >= max → releaseClaim + 跳过 | -| IT-POL-32 | per-PS syncLevel=5 | 6 个 VM 在同一 PS | 同时只有 5 个在执行 Agent 写入 | - ---- - -## 4. API 拦截器与 markDirty 联动 - -**覆盖约束**:Part 1b, C-PA - -### 4.1 API 成功触发 markDirty - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-INT-01 | APIUpdateVmInstanceMsg(改名) | 发送 API → 成功 | afterCompletion → markDirty(vmUuid, CONFIG) → dirty 行创建 | -| IT-INT-02 | APICreateVolumeSnapshotMsg | 快照创建成功 | markDirty(vmUuid, STORAGE) + storageStructureChange=true | -| IT-INT-03 | APIDeleteVolumeMsg | 删除数据盘成功 | markDirty(vmUuid, STORAGE) | - -### 4.2 API 失败不触发 markDirty - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-INT-10 | APIStopVmInstanceMsg 失败 | mock 停止失败 | afterCompletion 检测 reply.isSuccess()=false → 不 markDirty | -| IT-INT-11 | updateOnFailure=true 的 API 失败 | 批量 API 失败 | 仍然 markDirty | - -### 4.3 pendingApis 超时清理 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-INT-20 | API 45 分钟无 afterCompletion | 注入 pending → 等待 timeout | pendingApis 自动清理 + 补偿 markDirty(C-PA) | -| IT-INT-21 | 正常 API 不超时 | API 正常完成 | pendingApis 正常移除,无超时触发 | - ---- - -## 5. 存储迁移元数据链路 - -**覆盖约束**:Part 1c §1.4, C-01C-4 ~ C-01C-8 - -### 5.1 完整迁移 8 步 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-MIG-01 | sblk→sblk 迁移 | VM 在 PS-A 有元数据 | 8 步全部成功:PS-B 有完整元数据,PS-A 已清理 | -| IT-MIG-02 | local→NFS 迁移 | VM 在 local PS 有 JSON | PS-NFS 有 JSON,local 已清理 | -| IT-MIG-03 | 迁移期间 Poller 暂停 | 迁移开始 | nextRetryTime='2099-12-31',Poller 跳过该 VM | -| IT-MIG-04 | 迁移完成后 Poller 恢复 | 迁移成功 | nextRetryTime=NULL + markDirty(storageStructureChange=true) | - -### 5.2 迁移失败回滚 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-MIG-10 | Step 5 写入失败 | mock 目标 Agent 失败 | 回滚:deleteMetadata(目标) + nextRetryTime=NULL + markDirty(true) | -| IT-MIG-11 | Step 8 清理源端失败 | mock 源 Agent 失败 | WARN 日志,孤儿检测兜底(不阻塞迁移成功) | - -### 5.3 MN 重启恢复 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-MIG-20 | MN 重启时 nextRetryTime=2099 | DB 有暂停行 | managementNodeReady 重置 nextRetryTime=NULL(C-01C-8) | - ---- - -## 6. 注册端到端流程 - -**覆盖约束**:Part 3, C-03-1 ~ C-03-8 - -### 6.1 正常注册 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-REG-01 | 最小 VM 注册(根盘 only) | 有效 metadataContent JSON | VmInstanceVO(state=Stopped) + VolumeVO(Root) 已创建 | -| IT-REG-02 | 含快照链的 VM 注册 | 根盘 + 5 个快照 | 所有 VolumeSnapshotVO 按树结构创建 | -| IT-REG-03 | 含数据盘的 VM 注册 | 根盘 + 2 数据盘 | 3 个 VolumeVO 创建 | -| IT-REG-04 | 注册后 markDirty 触发 | 注册成功 | dirty 行已创建(storageStructureChange=true) | -| IT-REG-05 | 注册后 ConsistencyCheck | 注册成功 | 异步触发 ConsistencyCheck(C-03-7) | -| IT-REG-06 | registered.not.started ResourceConfig | 注册成功 | ResourceConfig 存在 → 后续 API 不触发 markDirty | - -### 6.2 注册拒绝 - -| 用例 ID | 场景 | 输入 | 期望 | -|---------|------|------|------| -| IT-REG-10 | UUID 冲突(正常资源) | vmUuid 已存在且 state≠Registering | 拒绝 + 错误码 | -| IT-REG-11 | 跨存储拒绝 | 根盘在 PS-A,数据盘在 PS-B | `CROSS_STORAGE_REJECTED` + expected/actual PS UUIDs(C-03-2) | -| IT-REG-12 | Root installPath 不存在 | mock Agent 返回 false | BLOCK(拒绝注册)(C-03-6) | -| IT-REG-13 | readStatus=CORRUPTED | metadata.__readStatus="CORRUPTED" | 拒绝注册 | -| IT-REG-14 | schemaVersion 不匹配 | version=999 | 拒绝注册(未设 forceVersionMismatch) | -| IT-REG-15 | forceVersionMismatch=true | version=999 | 允许注册,warnings 列出忽略字段 | - -### 6.3 注册回滚 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-REG-20 | 变基失败触发回滚 | mock qemu-img rebase 失败 | 所有 VO 按"由外到内"删除(C-03-4) | -| IT-REG-21 | Registering 遗留 UUID 冲突 → 回滚重试 | DB 有 state=Registering 的 VM | 自动回滚 → 重新注册 | -| IT-REG-22 | MN 重启扫描 Registering | DB 有 Registering VM | managementNodeReady 触发回滚 | -| IT-REG-23 | 回滚保留 TreeVO(其他 VM 共享) | TreeVO 下有其他 VM 的 ReferenceVO | 仅删除当前 VM 的 ReferenceVO,TreeVO 保留 | - ---- - -## 7. 路径指纹巡检端到端 - -**覆盖约束**:Part 2b §8.2, C-02B-3 - -### 7.1 正常巡检 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-FP-01 | 无漂移 | VM 已 flush + fingerprint 记录 | 巡检通过,不 markDirty | -| IT-FP-02 | installPath 变更 | 手动修改 VolumeVO.installPath | 巡检检测 drift → markDirty | -| IT-FP-03 | keyset 分页遍历 | 510 个 VM(batchSize=500) | 分 2 批遍历完所有 VM | - -### 7.2 边界 - -| 用例 ID | 场景 | 前置条件 | 期望 | -|---------|------|----------|------| -| IT-FP-10 | VM 从未 flush | 无 fingerprint 记录 | 巡检跳过 | -| IT-FP-11 | VM 已销毁(FK CASCADE) | VM 物理删除 | fingerprint 行自动级联删除 | - ---- - -## 8. API 端到端 - -**覆盖约束**:Part 5 - -### 8.1 扫描 API - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-API-01 | 扫描 sblk PS | APIScanVmInstanceMetadataMsg(psUuid) | 返回 vmUuid + vmName + vmCategory 列表 | -| IT-API-02 | 扫描空 PS | 无元数据的 PS | 返回空列表 | - -### 8.2 读取 API - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-API-10 | 读取正常元数据 | APIReadVmInstanceMetadataMsg | readStatus=OK + 完整 JSON | -| IT-API-11 | 读取损坏元数据 | 双 Slot 损坏 | readStatus=CORRUPTED | - -### 8.3 一致性检查 API - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-API-20 | 一致时 | APICheckVmInstanceMetadataConsistencyMsg | 报告一致 | -| IT-API-21 | 不一致 + autoRepair=true | DB 与存储不一致 | 检测到差异 + 自动 markDirty | - -### 8.4 手动更新 API - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-API-30 | APIUpdateVmMetadataMsg | 指定 vmUuid | markDirty 触发 → Poller flush → 存储更新 | - -### 8.5 清理 API - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| IT-API-40 | enabled=false 时清理 | APICleanupVmInstanceMetadataMsg | 存储 + DB 清理成功 | -| IT-API-41 | enabled=true 时拒绝 | 同上 | `METADATA_CLEANUP_REJECTED_WHILE_ENABLED`(C-02B-12) | diff --git "a/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" "b/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" deleted file mode 100644 index 3948d45a99d..00000000000 --- "a/docs/design/vm-metadata-07c-\346\225\205\351\232\234\346\263\250\345\205\245\346\265\213\350\257\225.md" +++ /dev/null @@ -1,183 +0,0 @@ -# VM 元数据 — 故障注入测试计划 - -> 故障注入测试验证系统在异常/极端条件下的安全恢复能力。需要 mock Agent 故障、模拟 MN 重启、注入 DB 异常。 -> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),性能测试见 [Part 7d](vm-metadata-07d-性能与补充测试.md)。 - -## 目录 - -1. [sblk 写入中断与 Crash Recovery](#1-sblk-写入中断与-crash-recovery) -2. [MN 重启恢复](#2-mn-重启恢复) -3. [双 MN 故障转移](#3-双-mn-故障转移) -4. [DB 异常](#4-db-异常) -5. [Agent 异常](#5-agent-异常) -6. [功能开关切换竞态](#6-功能开关切换竞态) - ---- - -## 1. sblk 写入中断与 Crash Recovery - -**覆盖约束**:Part 4c §3 三阶段写入, Part 4d §4.1 崩溃场景矩阵 - -### 1.1 三阶段崩溃点 - -sblk 写入分 3 个阶段:Phase 1(写 Inactive Slot)→ Phase 2(更新 Header: WriteSequence+1, PendingOp 设置)→ Phase 3(切换 ActiveSlot, 清除 PendingOp)。以下测试在每个阶段注入中断。 - -| 用例 ID | 崩溃点 | PendingOp | 恢复后 readStatus | 数据状态 | -|---------|--------|-----------|-------------------|----------| -| FI-SBLK-01 | Phase 1 中断(写 Slot 中途) | 0(Header 未更新) | OK | 旧 Slot 数据完好,未碰旧 Header | -| FI-SBLK-02 | Phase 1 完成、Phase 2 前中断 | 0 | OK | Inactive Slot 有新数据但 Header 未指向它 | -| FI-SBLK-03 | Phase 2 完成、Phase 3 前中断(CONFIG_UPDATE) | 1 | NEED_REPAIR | Inactive Slot 有新数据,Header PendingOp=1 未清除 | -| FI-SBLK-04 | Phase 2 完成、Phase 3 前中断(STORAGE_CHANGE) | 2 | NEED_REPAIR 或 DEGRADED | 取决于 Slot 数据完整性 | -| FI-SBLK-05 | Phase 3 部分写入(Header 4KB 写未完成) | 不确定 | DEGRADED | ControlChecksum 失败 → 降级使用另一 Slot | - -### 1.2 恢复操作 - -| 用例 ID | 场景 | 注入方式 | 步骤 | 期望 | -|---------|------|----------|------|------| -| FI-SBLK-10 | PendingOp=1 恢复 | 手动构造 Header(PendingOp=1, ActiveSlot=0) | readMetadata → 触发 repair | repair 完成 Phase 3 → ActiveSlot 切换 → PendingOp=0 → readStatus=OK 或 RECOVERED | -| FI-SBLK-11 | PendingOp=2 恢复 | 手动构造 Header(PendingOp=2) | readMetadata → 触发 repair | repair 尝试 Phase 3 + read-back 校验 → 成功则 OK,否则 DEGRADED | -| FI-SBLK-12 | 双 Slot 均损坏 | 篡改两个 Slot 的 checksum | readMetadata | readStatus=CORRUPTED | -| FI-SBLK-13 | Active Slot 损坏、Inactive 完好 | 篡改 Active Slot checksum | readMetadata | 降级使用 Inactive Slot → readStatus=DEGRADED | - -### 1.3 LV Extend 期间崩溃 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-SBLK-20 | lvextend 成功、Phase 1 前中断 | 扩容后未写入新数据 | 下次写入基于新布局,旧数据仍通过旧布局可读 | -| FI-SBLK-21 | lvextend 失败(空间不足) | mock lvextend 失败 | 写入返回错误,Header 保持旧布局不变 | -| FI-SBLK-22 | 扩容后读取尝试双布局 | 扩容后 Header 更新前中断 | readMetadata 先尝试 old-layout → 失败 → retry new-layout | - ---- - -## 2. MN 重启恢复 - -### 2.1 Registering 状态清理 - -**覆盖约束**:Part 3 §4 - -| 用例 ID | 场景 | 初始状态 | MN 重启后行为 | 期望 | -|---------|------|----------|--------------|------| -| FI-MN-01 | 本 MN 的 Registering VM | VmInstanceVO(state=Registering, registeringMnUuid=本MN) | managementNodeReady 回滚 | 所有关联 VO 删除(由外到内),VmInstanceVO 删除 | -| FI-MN-02 | 其他 MN(已离线)的 Registering VM | registeringMnUuid=MN-B且MN-B不在线 | managementNodeReady 回滚 | 同上 | -| FI-MN-03 | 其他 MN(仍在线)的 Registering VM | registeringMnUuid=MN-B且MN-B在线 | 跳过 | 不回滚(MN-B 仍在处理) | -| FI-MN-04 | 注册 Step 3 后崩溃(VmInstanceVO + VolumeVO 已创建,快照未创建) | DB 含部分 VO | 回滚 | VolumeVO + VmInstanceVO 删除,无快照残留 | -| FI-MN-05 | 注册 Step 5 后崩溃(所有 VO 已创建,变基未执行) | DB 含全部 VO | 回滚 | 所有 VO 按序删除 | - -### 2.2 Poller 暂停行恢复 - -**覆盖约束**:C-01C-8 - -| 用例 ID | 场景 | 初始状态 | MN 重启后行为 | 期望 | -|---------|------|----------|--------------|------| -| FI-MN-10 | 迁移暂停行存在 | dirty 行 nextRetryTime='2099-12-31T00:00:00' | managementNodeReady 重置 | nextRetryTime=NULL,Poller 恢复处理 | -| FI-MN-11 | 正常退避行不受影响 | dirty 行 nextRetryTime=明天 | managementNodeReady | 不修改(仅匹配 2099 魔数值) | - -### 2.3 lastFlushFailed 恢复链 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-MN-20 | MN 重启后 StaleRecoveryTask 启动 | lastFlushFailed=true 行存在 | StaleRecoveryTask 扫描 → markDirty(retryCount=0) → Poller 重新 flush | -| FI-MN-21 | staleRecoveryCount 达上限 | staleRecoveryCount=10 | StaleRecoveryTask 不再重入队 → WARN 日志 → 等待手动 APIUpdateVmMetadataMsg | - -### 2.4 升级全量刷新 - -**覆盖约束**:Part 2b §9.1 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-MN-30 | 升级刷新中途 MN 崩溃 | 处理到第 500 个 VM 时崩溃 | 重启后 lastRefreshVersion 仍为旧值 → 重新触发全量刷新(Δ-8 保障) | -| FI-MN-31 | 滚动升级 recent-nodeLeft 防护 | MN-A(v2) 启动,15 分钟内有 MN-B(v1) nodeLeft | 延迟 10 分钟重新检查,不立即执行全量刷新 | - ---- - -## 3. 双 MN 故障转移 - -**覆盖约束**:Part 2b §7, C-02B-1, C-02B-2 - -### 3.1 MN 宕机接管 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-HA-01 | MN-A 认领 dirty 行后宕机 | MN-A claim dirty(vm-1) → MN-A 下线 | FK SET NULL → MN-B nodeLeft 延迟 5s → claimAndFlush → vm-1 flush 成功 | -| FI-HA-02 | 接管延迟验证 | 同上 | 总接管时间 ≈ 心跳超时(~30s) + 5s ≈ 35s | -| FI-HA-03 | Fence Check 拦截 zombie 写入 | MN-A GC pause 恢复后尝试写入 | dirty 行 managementNodeUuid 已不是 MN-A → abort(C-02B-2) | - -### 3.2 脑裂防护 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-HA-10 | 两个 MN 同时 CAS 认领同一行 | 并发 UPDATE WHERE uuid=x AND managementNodeUuid IS NULL | 只有一个 affected_rows=1,另一个=0 | -| FI-HA-11 | GC pause 期间对端接管后并发写入 | MN-A pause → MN-B 接管写入 → MN-A 恢复写入 | sblk WriteSequence 保证最终一致(更高 SeqNum 胜出) | - -### 3.3 nodeLeft 延迟配置 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-HA-20 | nodeLeft.delaySec=0 | 配置延迟为 0 | 立即接管(增大竞态风险,仅验证可配置性) | -| FI-HA-21 | nodeLeft.delaySec=10 | 配置延迟为 10s | 10s 后接管 | - ---- - -## 4. DB 异常 - -### 4.1 markDirty 并发竞态 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-DB-01 | INSERT IGNORE 竞态(两个 MN 同时 INSERT 同一 VM) | 并发 markDirty | 一个 INSERT 成功,一个 IGNORE → 两者 UPDATE 均安全(C-DM-01) | -| FI-DB-02 | INSERT=0 且 UPDATE=0 | 极端竞态:INSERT IGNORE 后 UPDATE 前行被删除 | 重新 INSERT IGNORE(C-DM-01 保障) | - -### 4.2 FK CASCADE 验证 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-DB-10 | VM 物理删除级联清理 dirty 行 | DELETE VmInstanceEO | VmMetadataDirtyVO 行自动删除 | -| FI-DB-11 | VM 物理删除级联清理 fingerprint | DELETE VmInstanceEO | VmMetadataPathFingerprintVO 行自动删除 | -| FI-DB-12 | MN 离线级联释放认领 | DELETE ManagementNodeVO | dirty 行 managementNodeUuid=NULL(FK SET_NULL) | - ---- - -## 5. Agent 异常 - -### 5.1 Agent 不可达 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-AGT-01 | 写入时 Agent 超时 | mock Agent 不响应 | UpdateVmInstanceMetadataOnHypervisorMsg 超时(2min) → onFlushFailure | -| FI-AGT-02 | Agent 返回未知错误码 | mock Agent 返回 500 | onFlushFailure → 进入退避 | -| FI-AGT-03 | 扫描时 Agent 超时 | APIScanVmInstanceMetadataMsg + mock Agent 不响应 | API 超时返回错误 | - -### 5.2 Agent 部分成功 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-AGT-10 | 写入 Agent 成功但 MN 在收到响应前崩溃 | mock:Agent 写入完成 → MN 崩溃 | 存储有新数据但 dirty 行未删除 → MN 重启后 Poller 重新 flush(幂等覆盖写) | -| FI-AGT-11 | 读取 Agent 返回损坏数据 | mock Agent 返回非 JSON | readMetadata 报错,readStatus=CORRUPTED | - -### 5.3 PS 不可达 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-AGT-20 | PS 卸载期间 flush | PS Detached | flush 失败 → 退避 → stale → 最终熔断(staleRecoveryCount >= maxCycles) | -| FI-AGT-21 | PS 重新挂载后恢复 | PS Reattach + API 触发 | markDirty → Poller flush 成功 | - ---- - -## 6. 功能开关切换竞态 - -**覆盖约束**:Part 2b §9a, C-02B-11 ~ C-02B-13 - -### 6.1 快速 toggle - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-TOG-01 | false→true→false 快速切换 | 启用后立即禁用 | 初始化任务检测到 enabled=false → 中止(C-02B-13) | -| FI-TOG-02 | false→true→false→true | 两次启用 | 第二次初始化:LEFT JOIN 排除已有 dirty 行 → 仅初始化新 VM | - -### 6.2 true→false 清理 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| FI-TOG-10 | 禁用时清理 PathFingerprint | true→false | 异步批量删除所有 VmMetadataPathFingerprintVO(Δ-10) | -| FI-TOG-11 | 禁用时 dirty 行保留 | true→false | VmMetadataDirtyVO 行不删除 | -| FI-TOG-12 | 禁用时存储元数据保留 | true→false | sblk LV / JSON 文件不删除 | diff --git "a/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" "b/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" deleted file mode 100644 index 97228f7dd1d..00000000000 --- "a/docs/design/vm-metadata-07d-\346\200\247\350\203\275\344\270\216\350\241\245\345\205\205\346\265\213\350\257\225.md" +++ /dev/null @@ -1,261 +0,0 @@ -# VM 元数据 — 性能与补充测试计划 - -> 性能基准测试和未归入前三类的补充测试场景。 -> 单元测试见 [Part 7a](vm-metadata-07a-单元测试计划.md),集成测试见 [Part 7b](vm-metadata-07b-集成测试计划.md),故障注入见 [Part 7c](vm-metadata-07c-故障注入测试.md)。 - -## 目录 - -1. [性能基准:全量元数据更新](#1-性能基准全量元数据更新) -2. [性能基准:升级批次压力](#2-性能基准升级批次压力) -3. [性能基准:注册流程](#3-性能基准注册流程) -4. [性能基准:Poller 吞吐](#4-性能基准poller-吞吐) -5. [性能基准:sblk 读写延迟](#5-性能基准sblk-读写延迟) -6. [补充:E2E 场景测试](#6-补充e2e-场景测试) -7. [补充:数据迁移与兼容性](#7-补充数据迁移与兼容性) -8. [补充:安全与权限](#8-补充安全与权限) -9. [补充:可观测性验证](#9-补充可观测性验证) -10. [补充:GlobalConfig 动态生效](#10-补充globalconfig-动态生效) - ---- - -## 1. 性能基准:全量元数据更新 - -### 1.1 1000 VM 全量 markDirty - -| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | -|---------|------|------|----------|------------| -| PERF-01 | 1000 VM 批量 markDirty 耗时 | 单 MN,MySQL | markDirty 全部完成时间 | < 10s | -| PERF-02 | 1000 VM 批量 markDirty(Galera 双节点) | 双 MN | 同上 + 无死锁 | < 15s | -| PERF-03 | 1000 VM markDirty 后 Poller 全部消化 | pollInterval=5s, maxConcurrent=10, ps.maxConcurrent=5 | 从首个 markDirty 到最后一个 dirty 行删除 | < 15 分钟 | - -### 1.2 10000 VM 大规模验证 - -| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | -|---------|------|------|----------|------------| -| PERF-04 | 10000 VM markDirty | 双 MN | 插入完成时间 | < 60s | -| PERF-05 | 10000 VM Poller 消化 | 双 MN, maxConcurrent=10 | 全部 flush 完成时间 | < 2.5 小时 | - -### 1.3 单 VM flush 延迟分布 - -| 用例 ID | 场景 | 度量指标 | 基准 | -|---------|------|----------|------| -| PERF-06 | 普通 VM(1 根盘、少量快照)flush 延迟 | buildMetadata + Agent 写入总耗时 | P50 < 500ms, P99 < 3s | -| PERF-07 | 大 VM(24 盘、256 快照)flush 延迟 | 同上 | P50 < 5s, P99 < 15s | - ---- - -## 2. 性能基准:升级批次压力 - -**覆盖约束**:Part 2b §9.2, C-02B-4 - -### 2.1 升级全量刷新 - -| 用例 ID | 场景 | 配置 | 度量指标 | 基准(P95) | -|---------|------|------|----------|------------| -| PERF-10 | 1000 VM 升级全量 markDirty | batchSize=1000 | INSERT IGNORE + UPDATE 总耗时 | < 5s | -| PERF-11 | 10000 VM 升级全量 markDirty | batchSize=1000 | 10 批总耗时 | < 30s | -| PERF-12 | 升级全量 markDirty 期间业务 API 影响 | PERF-11 同时运行 100 个 API | API 响应延迟增幅 | < 20% | - -### 2.2 false→true 初始化 - -| 用例 ID | 场景 | 配置 | 度量指标 | 基准 | -|---------|------|------|----------|------| -| PERF-15 | 5000 VM 初始化 | initBatchSize=200, batchDelay=5s | 初始化完成时间 | ≈ 25 批 × 5s = ~125s | -| PERF-16 | 初始化期间 Poller 吞吐 | 同上 | dirty 行积压量峰值 | < 500 行(证明批间延迟有效) | - ---- - -## 3. 性能基准:注册流程 - -### 3.1 注册耗时 - -| 用例 ID | 场景 | 输入规模 | 度量指标 | 基准(P95) | -|---------|------|----------|----------|------------| -| PERF-20 | 最小 VM 注册 | 1 根盘、0 快照 | 注册总耗时(Step 1-7) | < 3s | -| PERF-21 | 中等 VM 注册 | 4 盘、50 快照 | 同上 | < 10s | -| PERF-22 | 极端 VM 注册 | 24 盘、256 快照 + Group + Ref | 同上 | < 60s | -| PERF-23 | 极端 VM UUID 冲突检测 | ~7000 UUID(分批 1000/批) | 冲突检测耗时 | < 2s | - -### 3.2 注册回滚耗时 - -| 用例 ID | 场景 | 输入规模 | 度量指标 | 基准 | -|---------|------|----------|----------|------| -| PERF-25 | 大 VM 回滚 | 24 盘、256 快照全部已创建 | 由外到内删除总耗时 | < 30s | - ---- - -## 4. 性能基准:Poller 吞吐 - -### 4.1 Poller 轮询效率 - -| 用例 ID | 场景 | 度量指标 | 基准 | -|---------|------|----------|------| -| PERF-30 | 空 Poller 周期(0 dirty 行) | SELECT 查询耗时 | < 1ms | -| PERF-31 | 满载 Poller 周期(50 行认领) | claim + submit 总耗时 | < 100ms | -| PERF-32 | 大量退避行跳过 | 500 dirty 行中 450 行有 nextRetryTime > now | WHERE 过滤效率 | < 5ms | - -### 4.2 路径指纹巡检效率 - -| 用例 ID | 场景 | 度量指标 | 基准 | -|---------|------|----------|------| -| PERF-35 | 1000 VM 巡检(无 drift) | 全量巡检耗时 | < 5s | -| PERF-36 | 5000 VM 巡检 keyset 分页 | 分页查询 + 比对总耗时 | < 20s | -| PERF-37 | 巡检期间零存储 I/O 验证 | Agent 调用计数 | 0 次 Agent 调用 | - ---- - -## 5. 性能基准:sblk 读写延迟 - -### 5.1 写入延迟 - -| 用例 ID | 场景 | Payload 大小 | 度量指标 | 基准 | -|---------|------|-------------|----------|------| -| PERF-40 | 小 payload 写入 | 10KB | Agent pwrite 耗时 | < 10ms | -| PERF-41 | 中等 payload 写入 | 500KB | 同上 | < 50ms | -| PERF-42 | 大 payload 写入 | 5MB | 同上(含 lvextend) | < 500ms | - -### 5.2 读取延迟 - -| 用例 ID | 场景 | 度量指标 | 基准 | -|---------|------|----------|------| -| PERF-45 | 正常读取(PendingOp=0) | pread + 解析耗时 | < 20ms | -| PERF-46 | 带 repair 的读取(PendingOp=1) | repair + pread 总耗时 | < 100ms | - -### 5.3 扫描效率 - -| 用例 ID | 场景 | 度量指标 | 基准 | -|---------|------|----------|------| -| PERF-50 | 100 LV 扫描(仅读 Header 摘要区) | scanMetadataVmUuids 总耗时 | < 2s | -| PERF-51 | 1000 LV 扫描 | 同上 | < 15s | - ---- - -## 6. 补充:E2E 场景测试 - -### 6.1 完整生命周期 - -| 用例 ID | 场景 | 步骤 | 验证点 | -|---------|------|------|--------| -| E2E-01 | VM 创建→运行→改名→加盘→快照→迁移→销毁 | 全流程 | 每步后元数据正确反映 DB 状态 | -| E2E-02 | VM 创建→销毁→恢复→再销毁→Expunge | Destroy→Recover→Destroy→Expunge | Recover 后元数据恢复更新;Expunge 时 deleteMetadata | -| E2E-03 | 链式克隆子 VM 注册 | 从存储扫描→读取→注册子 VM | ReferenceVO(parentId=null) + TreeVO 幂等 | -| E2E-04 | VM 注册→首次启动→markDirty 触发 | 注册完成→启动 VM→Running | `registered.not.started` Config 删除 → markDirty → Poller flush | - -### 6.2 多 MN 协同场景 - -| 用例 ID | 场景 | 步骤 | 验证点 | -|---------|------|------|--------| -| E2E-10 | 双 MN 分摊 dirty 行处理 | 20 个 VM markDirty + 双 MN Poller | 所有 VM 最终 flush 成功,无遗漏 | -| E2E-11 | MN-A flush 中 → MN-A 宕机 → MN-B 接管 | in-flight flush 场景 | MN-B 接管并成功 flush | - -### 6.3 存储迁移 + 元数据联动 - -| 用例 ID | 场景 | 步骤 | 验证点 | -|---------|------|------|--------| -| E2E-20 | 根盘迁移 sblk→sblk | 迁移成功后读取目标 PS 元数据 | 元数据内容完整 + 源 PS 已清理 | -| E2E-21 | 根盘迁移失败回滚 | Step 5 写入失败 | 源 PS 元数据不变 + 目标 PS 残留清理 + Poller 恢复 | -| E2E-22 | 仅数据盘迁移 | 数据盘从 PS-A → PS-B(根盘不动) | 元数据更新到根盘所在 PS,storageStructureChange=true | - ---- - -## 7. 补充:数据迁移与兼容性 - -### 7.1 版本兼容 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| COMPAT-01 | 旧版 ZStack 写入的 sblk → 新版读取 | 用旧格式写入 → 新版 readMetadata | 正常读取(schemaVersion 向后兼容) | -| COMPAT-02 | schemaVersion 低于当前的元数据注册 | forceVersionMismatch=true | 注册成功 + warnings 列出差异字段 | -| COMPAT-03 | schemaVersion 高于当前的元数据注册 | 来自更新版本的 JSON | 默认拒绝;forceVersionMismatch=true 时允许 | - -### 7.2 DB 升级 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| COMPAT-10 | 首次部署(新建表) | 全新安装 | VmMetadataDirtyVO + VmMetadataPathFingerprintVO 表创建成功 | -| COMPAT-11 | 升级部署(ALTER TABLE) | 从无元数据版本升级 | 新表正确创建;GlobalConfig 默认值生效 | - ---- - -## 8. 补充:安全与权限 - -### 8.1 API 权限 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| SEC-01 | 普通用户调用 APIRegisterVmInstanceFromMetadataMsg | 非 admin 账户 | 权限拒绝 | -| SEC-02 | 普通用户查询 Registering VM | QueryVmInstance | Registering VM 不可见 | -| SEC-03 | admin 查询 Registering VM | QueryVmInstance | Registering VM 可见 | -| SEC-04 | 普通用户调用 APIScanVmInstanceMetadataMsg | 非 admin 账户 | 权限拒绝 | - -### 8.2 注册安全 - -| 用例 ID | 场景 | 步骤 | 期望 | -|---------|------|------|------| -| SEC-10 | 恶意 JSON 注入 | metadataContent 含 SQL 注入 payload | ORM 参数化查询隔离,不影响 DB | -| SEC-11 | 超大 JSON(> 100MB) | metadataContent 超大 | API 层大小限制拦截 | -| SEC-12 | installPath 路径遍历 | installPath 含 `../../etc/passwd` | 前缀锚定替换 + 正则预校验拦截(C-03-3) | - ---- - -## 9. 补充:可观测性验证 - -**覆盖约束**:Part 2b §14 - -### 9.1 Prometheus 指标 - -| 用例 ID | 指标 | 场景 | 期望 | -|---------|------|------|------| -| OBS-01 | `vm_metadata_flush_total{status=success}` | 正常 flush | Counter 递增 | -| OBS-02 | `vm_metadata_flush_total{status=fail}` | Agent 失败 | Counter 递增 | -| OBS-03 | `vm_metadata_flush_duration_seconds` | 正常 flush | Histogram 记录耗时 | -| OBS-04 | `vm_metadata_dirty_queue_size` | markDirty 后 | Gauge > 0 | -| OBS-05 | `vm_metadata_registration_total{status=success}` | 注册成功 | Counter 递增 | -| OBS-06 | `vm_metadata_registration_total{status=rollback}` | 注册回滚 | Counter 递增 | - -### 9.2 日志验证 - -| 用例 ID | 场景 | 期望日志 | -|---------|------|----------| -| OBS-10 | flush 失败且重试耗尽 | ERROR 日志含 vmUuid + 失败原因 + retryCount | -| OBS-11 | Fence Check 拦截 | WARN `"Lost claim on vm {uuid}, abort flush write"` | -| OBS-12 | 路径漂移检测 | WARN `"path drift detected for VM [{uuid}]"` + 新旧 snapshot 对比 | -| OBS-13 | 孤儿元数据检测 | WARN `"orphan metadata detected: ps={}, vm={}, reason={}"` | -| OBS-14 | stale recovery 熔断 | WARN `"VM [{}] metadata stale recovery exceeded {} cycles, entering permanent-stale"` | - ---- - -## 10. 补充:GlobalConfig 动态生效 - -**覆盖约束**:C-RB-04, C-M4, 各 §13 配置项 - -| 用例 ID | 配置项 | 变更方式 | 期望 | -|---------|--------|----------|------| -| CFG-01 | `vm.metadata.dirty.pollIntervalSec` | 5→10 | 下轮 Poller 间隔变为 10s | -| CFG-02 | `vm.metadata.maxRetry` | 5→3 | 3 次失败后即标记 stale | -| CFG-03 | `vm.metadata.global.maxConcurrent` | 10→5 | AtomicInteger 上限立即生效 | -| CFG-04 | `vm.metadata.retry.baseDelaySeconds` | 10→20 | 退避间隔加倍 | -| CFG-05 | `vm.metadata.nodeLeft.delaySec` | 5→10 | nodeLeft 事件后延迟 10s 再接管 | -| CFG-06 | `vm.metadata.enabled` | true→false | Poller 停止处理 + PathFingerprint 异步清理 | -| CFG-07 | `vm.metadata.enabled` | false→true | 分批初始化启动 | -| CFG-08 | `vm.metadata.pendingApi.timeoutMinutes` | 45→30 | pendingApis 超时缩短 | -| CFG-09 | `vm.metadata.pathCheck.intervalSec` | 300→60 | 巡检频率加快 | -| CFG-10 | `vm.metadata.staleRecovery.maxCycles` | 10→3 | 熔断更快触发 | - ---- - -## 附录:测试用例 ID 编号规则 - -| 前缀 | 类别 | 文档 | -|------|------|------| -| UT-* | 单元测试 | Part 7a | -| IT-* | 集成测试 | Part 7b | -| FI-* | 故障注入测试 | Part 7c | -| PERF-* | 性能基准测试 | Part 7d | -| E2E-* | 端到端场景测试 | Part 7d §6 | -| COMPAT-* | 兼容性测试 | Part 7d §7 | -| SEC-* | 安全与权限测试 | Part 7d §8 | -| OBS-* | 可观测性验证 | Part 7d §9 | -| CFG-* | 配置动态生效 | Part 7d §10 | - -**总计**:约 **190+ 条测试用例**,覆盖序列化、存储协议、并发控制、故障恢复、性能基准、安全权限和可观测性全维度。 diff --git a/header/src/main/java/org/zstack/header/APIIsOpensourceVersionMsg.java b/header/src/main/java/org/zstack/header/APIIsOpensourceVersionMsg.java index 4974a4060d1..1df5b8d2082 100755 --- a/header/src/main/java/org/zstack/header/APIIsOpensourceVersionMsg.java +++ b/header/src/main/java/org/zstack/header/APIIsOpensourceVersionMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/5/17. @@ -14,6 +15,7 @@ responseClass = APIIsOpensourceVersionReply.class ) @SuppressCredentialCheck +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIIsOpensourceVersionMsg extends APISyncCallMessage { public static APIIsOpensourceVersionMsg __example__() { return new APIIsOpensourceVersionMsg(); diff --git a/header/src/main/java/org/zstack/header/allocator/APIGetCpuMemoryCapacityMsg.java b/header/src/main/java/org/zstack/header/allocator/APIGetCpuMemoryCapacityMsg.java index b616039dcb8..45a6319c0c2 100755 --- a/header/src/main/java/org/zstack/header/allocator/APIGetCpuMemoryCapacityMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/APIGetCpuMemoryCapacityMsg.java @@ -11,12 +11,14 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hosts/capacities/cpu-memory", method = HttpMethod.GET, responseClass = APIGetCpuMemoryCapacityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCpuMemoryCapacityMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = ZoneVO.class) private List zoneUuids; diff --git a/header/src/main/java/org/zstack/header/allocator/APIGetHostAllocatorStrategiesMsg.java b/header/src/main/java/org/zstack/header/allocator/APIGetHostAllocatorStrategiesMsg.java index 782900d315e..575d50e2055 100755 --- a/header/src/main/java/org/zstack/header/allocator/APIGetHostAllocatorStrategiesMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/APIGetHostAllocatorStrategiesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIGetHostAllocatorStrategiesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostAllocatorStrategiesMsg extends APISyncCallMessage { public static APIGetHostAllocatorStrategiesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/apimediator/APIIsReadyToGoMsg.java b/header/src/main/java/org/zstack/header/apimediator/APIIsReadyToGoMsg.java index 9aee1c4edde..73dfee82f94 100755 --- a/header/src/main/java/org/zstack/header/apimediator/APIIsReadyToGoMsg.java +++ b/header/src/main/java/org/zstack/header/apimediator/APIIsReadyToGoMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.managementnode.APIManagementNodeMessage; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @SuppressCredentialCheck @RestRequest( @@ -13,6 +14,7 @@ responseClass = APIIsReadyToGoReply.class, category = "other" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIIsReadyToGoMsg extends APISyncCallMessage implements APIManagementNodeMessage { private String managementNodeId; diff --git a/header/src/main/java/org/zstack/header/cluster/APIChangeClusterStateMsg.java b/header/src/main/java/org/zstack/header/cluster/APIChangeClusterStateMsg.java index 82078bbaab3..264d181f877 100755 --- a/header/src/main/java/org/zstack/header/cluster/APIChangeClusterStateMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APIChangeClusterStateMsg.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.MetadataImpact; /** * @api change state of cluster. See field 'state' of :ref:`ClusterInventory` for details. @@ -44,6 +45,7 @@ isAction = true, responseClass = APIChangeClusterStateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeClusterStateMsg extends APIMessage implements ClusterMessage { /** * @desc cluster uuid diff --git a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java index 18d1727a07f..cec5d865c15 100755 --- a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; /** * @api create a new cluster in zone @@ -48,6 +49,7 @@ method = HttpMethod.POST, responseClass = APICreateClusterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateClusterMsg extends APICreateMessage implements CreateClusterMessage, APIAuditor { /** * @desc uuid of zone this cluster is going to create in diff --git a/header/src/main/java/org/zstack/header/cluster/APIDeleteClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APIDeleteClusterMsg.java index f05ea2c5feb..f6a0ba8f036 100755 --- a/header/src/main/java/org/zstack/header/cluster/APIDeleteClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APIDeleteClusterMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api delete a cluster. All descendant resources, for example hosts/vm are deleted in cascade as well @@ -40,6 +41,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteClusterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteClusterMsg extends APIDeleteMessage implements ClusterMessage { /** * @desc cluster uuid diff --git a/header/src/main/java/org/zstack/header/cluster/APIQueryClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APIQueryClusterMsg.java index d678c7e4e2d..5f7aafbb786 100755 --- a/header/src/main/java/org/zstack/header/cluster/APIQueryClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APIQueryClusterMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryClusterReply.class, inventoryClass = ClusterInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryClusterReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryClusterMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterMsg.java index 5545bae2a30..03c6fdbf5d2 100755 --- a/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ isAction = true, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateClusterMsg extends APIMessage implements ClusterMessage { @APIParam(resourceType = ClusterVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterOSMsg.java b/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterOSMsg.java index acdd2e9db14..1f93203e52f 100644 --- a/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterOSMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APIUpdateClusterOSMsg.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * Created by GuoYi on 3/12/18 @@ -20,6 +21,7 @@ method = HttpMethod.PUT ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateClusterOSMsg extends APICreateMessage implements ClusterMessage { @APIParam(resourceType = ClusterVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APIChangeDiskOfferingStateMsg.java b/header/src/main/java/org/zstack/header/configuration/APIChangeDiskOfferingStateMsg.java index b79913b9a50..fe24cf81369 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIChangeDiskOfferingStateMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIChangeDiskOfferingStateMsg.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.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -17,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APIChangeDiskOfferingStateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeDiskOfferingStateMsg extends APIMessage implements DiskOfferingMessage { @APIParam(resourceType = DiskOfferingVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APIChangeInstanceOfferingStateMsg.java b/header/src/main/java/org/zstack/header/configuration/APIChangeInstanceOfferingStateMsg.java index b9fd4bb388b..49c0eb4bcc7 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIChangeInstanceOfferingStateMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIChangeInstanceOfferingStateMsg.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.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -17,6 +18,7 @@ isAction = true, responseClass = APIChangeInstanceOfferingStateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeInstanceOfferingStateMsg extends APIMessage implements InstanceOfferingMessage { @APIParam(resourceType = InstanceOfferingVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APICreateDiskOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APICreateDiskOfferingMsg.java index 25bbce0dd41..a94e5e5efff 100755 --- a/header/src/main/java/org/zstack/header/configuration/APICreateDiskOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APICreateDiskOfferingMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(DiskOfferingVO.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APICreateDiskOfferingEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDiskOfferingMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/configuration/APICreateInstanceOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APICreateInstanceOfferingMsg.java index 04ac35e790a..9c5b80e12ff 100755 --- a/header/src/main/java/org/zstack/header/configuration/APICreateInstanceOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APICreateInstanceOfferingMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.tag.TagResourceType; import static org.zstack.header.vm.VmInstanceConstant.USER_VM_TYPE; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(InstanceOfferingVO.class) @RestRequest( @@ -19,6 +20,7 @@ parameterName = "params", method = HttpMethod.POST ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateInstanceOfferingMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/configuration/APIDeleteDiskOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIDeleteDiskOfferingMsg.java index 1062d484730..984ba1fcde2 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIDeleteDiskOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIDeleteDiskOfferingMsg.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.MetadataImpact; @RestRequest( path = "/disk-offerings/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteDiskOfferingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteDiskOfferingMsg extends APIDeleteMessage implements DiskOfferingMessage { @APIParam(resourceType = DiskOfferingVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APIDeleteInstanceOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIDeleteInstanceOfferingMsg.java index cd8c24265b2..b7913ab2560 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIDeleteInstanceOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIDeleteInstanceOfferingMsg.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.MetadataImpact; @RestRequest( path = "/instance-offerings/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteInstanceOfferingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteInstanceOfferingMsg extends APIDeleteMessage implements InstanceOfferingMessage { @APIParam(resourceType = InstanceOfferingVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateApiJsonTemplateMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateApiJsonTemplateMsg.java index da93f1444b0..d8eafc3f241 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateApiJsonTemplateMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateApiJsonTemplateMsg.java @@ -4,6 +4,9 @@ import org.zstack.header.message.APIParam; import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateApiJsonTemplateMsg extends APIMessage { @APIParam(required = false) diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateApiTypeScriptDefinitionMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateApiTypeScriptDefinitionMsg.java index 71c0d7e58b3..0a4ba765c9f 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateApiTypeScriptDefinitionMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateApiTypeScriptDefinitionMsg.java @@ -1,9 +1,11 @@ package org.zstack.header.configuration; import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateApiTypeScriptDefinitionMsg extends APIMessage { private String outputPath; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateGroovyClassMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateGroovyClassMsg.java index 8f7b6ae3725..969eba32928 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateGroovyClassMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateGroovyClassMsg.java @@ -3,6 +3,7 @@ import org.zstack.header.message.APIMessage; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -10,6 +11,7 @@ * Time: 10:24 PM * To change this template use File | Settings | File Templates. */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateGroovyClassMsg extends APIMessage { private String outputPath; private List basePackageNames; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlForeignKeyMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlForeignKeyMsg.java index 06e5f69cc78..197a73e9ef3 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlForeignKeyMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlForeignKeyMsg.java @@ -4,9 +4,11 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateSqlForeignKeyMsg extends APIMessage { private String outputPath; private List basePackageNames; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlIndexMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlIndexMsg.java index 9b35bedbc30..919466a7482 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlIndexMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlIndexMsg.java @@ -3,9 +3,11 @@ import org.zstack.header.message.APIMessage; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateSqlIndexMsg extends APIMessage { private String outputPath; private List basePackageNames; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlVOViewMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlVOViewMsg.java index c88eabdb072..b244b4b8702 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlVOViewMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateSqlVOViewMsg.java @@ -4,9 +4,11 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateSqlVOViewMsg extends APIMessage { private List basePackageNames; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGenerateTestLinkDocumentMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGenerateTestLinkDocumentMsg.java index 1fddfc9af00..37dc98a5a7c 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIGenerateTestLinkDocumentMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGenerateTestLinkDocumentMsg.java @@ -1,6 +1,9 @@ package org.zstack.header.configuration; import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateTestLinkDocumentMsg extends APIMessage { private String outputDir; diff --git a/header/src/main/java/org/zstack/header/configuration/APIGetGlobalPropertyMsg.java b/header/src/main/java/org/zstack/header/configuration/APIGetGlobalPropertyMsg.java index d5acb690fa4..c5cd7a994c6 100644 --- a/header/src/main/java/org/zstack/header/configuration/APIGetGlobalPropertyMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIGetGlobalPropertyMsg.java @@ -1,10 +1,12 @@ package org.zstack.header.configuration; import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/23/2015. */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetGlobalPropertyMsg extends APISyncCallMessage { public static APIGetGlobalPropertyMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/configuration/APIQueryDiskOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIQueryDiskOfferingMsg.java index 035ce082135..0dfc1a97352 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIQueryDiskOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIQueryDiskOfferingMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryDiskOfferingReply.class, inventoryClass = DiskOfferingInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryDiskOfferingReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryDiskOfferingMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/configuration/APIQueryInstanceOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIQueryInstanceOfferingMsg.java index 50ed0c52914..08fb5609263 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIQueryInstanceOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIQueryInstanceOfferingMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryInstanceOfferingReply.class, inventoryClass = InstanceOfferingInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryInstanceOfferingReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryInstanceOfferingMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/configuration/APIUpdateDiskOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIUpdateDiskOfferingMsg.java index a72130cf9f6..267171c64ce 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIUpdateDiskOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIUpdateDiskOfferingMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateDiskOfferingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateDiskOfferingMsg extends APIMessage implements DiskOfferingMessage { @APIParam(resourceType = DiskOfferingVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/configuration/APIUpdateInstanceOfferingMsg.java b/header/src/main/java/org/zstack/header/configuration/APIUpdateInstanceOfferingMsg.java index 26da5d39f3a..8b41303cd2b 100755 --- a/header/src/main/java/org/zstack/header/configuration/APIUpdateInstanceOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/configuration/APIUpdateInstanceOfferingMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateInstanceOfferingMsg extends APIMessage implements InstanceOfferingMessage { @APIParam(resourceType = InstanceOfferingVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/console/APIQueryConsoleProxyAgentMsg.java b/header/src/main/java/org/zstack/header/console/APIQueryConsoleProxyAgentMsg.java index 7a93127b197..ef744576303 100755 --- a/header/src/main/java/org/zstack/header/console/APIQueryConsoleProxyAgentMsg.java +++ b/header/src/main/java/org/zstack/header/console/APIQueryConsoleProxyAgentMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/3/15. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryConsoleProxyAgentReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryConsoleProxyAgentMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/console/APIReconnectConsoleProxyAgentMsg.java b/header/src/main/java/org/zstack/header/console/APIReconnectConsoleProxyAgentMsg.java index 94a9f7b11b0..90aada1f4c5 100755 --- a/header/src/main/java/org/zstack/header/console/APIReconnectConsoleProxyAgentMsg.java +++ b/header/src/main/java/org/zstack/header/console/APIReconnectConsoleProxyAgentMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/3/15. @@ -18,6 +19,7 @@ responseClass = APIReconnectConsoleProxyAgentEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectConsoleProxyAgentMsg extends APIMessage implements ConsoleProxyAgentMessage { @APIParam(required = false, nonempty = true) private List agentUuids; diff --git a/header/src/main/java/org/zstack/header/console/APIRequestConsoleAccessMsg.java b/header/src/main/java/org/zstack/header/console/APIRequestConsoleAccessMsg.java index dd24182622b..6db1f93f7fd 100755 --- a/header/src/main/java/org/zstack/header/console/APIRequestConsoleAccessMsg.java +++ b/header/src/main/java/org/zstack/header/console/APIRequestConsoleAccessMsg.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.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -18,6 +19,7 @@ parameterName = "params", responseClass = APIRequestConsoleAccessEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRequestConsoleAccessMsg extends APIMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/console/APIUpdateConsoleProxyAgentMsg.java b/header/src/main/java/org/zstack/header/console/APIUpdateConsoleProxyAgentMsg.java index 3ca2ff726e4..7fe9f9075b9 100755 --- a/header/src/main/java/org/zstack/header/console/APIUpdateConsoleProxyAgentMsg.java +++ b/header/src/main/java/org/zstack/header/console/APIUpdateConsoleProxyAgentMsg.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.MetadataImpact; @RestRequest( path = "/consoles/agents/{uuid}/actions", @@ -11,6 +12,7 @@ responseClass = APIUpdateConsoleProxyAgentEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateConsoleProxyAgentMsg extends APIMessage implements ConsoleProxyAgentMessage { @APIParam(resourceType = ConsoleProxyAgentVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/core/APIGetChainTaskMsg.java b/header/src/main/java/org/zstack/header/core/APIGetChainTaskMsg.java index 6033a098680..c0351d9ed9e 100644 --- a/header/src/main/java/org/zstack/header/core/APIGetChainTaskMsg.java +++ b/header/src/main/java/org/zstack/header/core/APIGetChainTaskMsg.java @@ -10,12 +10,14 @@ import java.util.function.Function; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/core/task-details", method = HttpMethod.GET, responseClass = APIGetChainTaskReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetChainTaskMsg extends APISyncCallMessage { @APIParam(nonempty = false, required = false) private List syncSignatures; diff --git a/header/src/main/java/org/zstack/header/core/encrypt/APIGetEncryptedFieldMsg.java b/header/src/main/java/org/zstack/header/core/encrypt/APIGetEncryptedFieldMsg.java index 115691861f3..e1ea6f458f4 100644 --- a/header/src/main/java/org/zstack/header/core/encrypt/APIGetEncryptedFieldMsg.java +++ b/header/src/main/java/org/zstack/header/core/encrypt/APIGetEncryptedFieldMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @author hanyu.liang @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetEncryptedFieldReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetEncryptedFieldMsg extends APISyncCallMessage { @APIParam(required = false) private String encryptedType; diff --git a/header/src/main/java/org/zstack/header/core/encrypt/APIUpdateEncryptKeyMsg.java b/header/src/main/java/org/zstack/header/core/encrypt/APIUpdateEncryptKeyMsg.java index 796e3f8da1f..8cb8eaf08d2 100644 --- a/header/src/main/java/org/zstack/header/core/encrypt/APIUpdateEncryptKeyMsg.java +++ b/header/src/main/java/org/zstack/header/core/encrypt/APIUpdateEncryptKeyMsg.java @@ -2,10 +2,12 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 16/12/28. */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateEncryptKeyMsg extends APIMessage { @APIParam diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIGetExternalServicesMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIGetExternalServicesMsg.java index d9705d1d658..73c2cadbc9f 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/APIGetExternalServicesMsg.java +++ b/header/src/main/java/org/zstack/header/core/external/service/APIGetExternalServicesMsg.java @@ -3,12 +3,14 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/external/services", method = HttpMethod.GET, responseClass = APIGetExternalServicesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetExternalServicesMsg extends APISyncCallMessage { public static APIGetExternalServicesMsg __example__() { return new APIGetExternalServicesMsg(); diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIReloadExternalServiceMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIReloadExternalServiceMsg.java index de3c8c0019f..9c2567c3665 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/APIReloadExternalServiceMsg.java +++ b/header/src/main/java/org/zstack/header/core/external/service/APIReloadExternalServiceMsg.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.MetadataImpact; @RestRequest( path = "/external/services", @@ -11,6 +12,7 @@ responseClass = APIReloadExternalServiceEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReloadExternalServiceMsg extends APIMessage { @APIParam private String name; diff --git a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressMsg.java b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressMsg.java index f9fdc6b1564..7f13b7816f0 100755 --- a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressMsg.java +++ b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/3/21. @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetTaskProgressReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetTaskProgressMsg extends APISyncCallMessage { private String apiId; diff --git a/header/src/main/java/org/zstack/header/core/webhooks/APICreateWebhookMsg.java b/header/src/main/java/org/zstack/header/core/webhooks/APICreateWebhookMsg.java index 2c6bfca7eba..f3bf12a84ca 100755 --- a/header/src/main/java/org/zstack/header/core/webhooks/APICreateWebhookMsg.java +++ b/header/src/main/java/org/zstack/header/core/webhooks/APICreateWebhookMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APICreateMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/5/7. @@ -15,6 +16,7 @@ responseClass = APICreateWebhookEvent.class ) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateWebhookMsg extends APICreateMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/core/webhooks/APIDeleteWebhookMsg.java b/header/src/main/java/org/zstack/header/core/webhooks/APIDeleteWebhookMsg.java index bef853c3d2d..ee804a3f9b7 100755 --- a/header/src/main/java/org/zstack/header/core/webhooks/APIDeleteWebhookMsg.java +++ b/header/src/main/java/org/zstack/header/core/webhooks/APIDeleteWebhookMsg.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.MetadataImpact; /** * Created by xing5 on 2017/5/7. @@ -14,6 +15,7 @@ responseClass = APIDeleteWebhookEvent.class ) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteWebhookMsg extends APIDeleteMessage { @APIParam(resourceType = WebhookVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/core/webhooks/APIQueryWebhookMsg.java b/header/src/main/java/org/zstack/header/core/webhooks/APIQueryWebhookMsg.java index f417195e748..8545308c3fb 100755 --- a/header/src/main/java/org/zstack/header/core/webhooks/APIQueryWebhookMsg.java +++ b/header/src/main/java/org/zstack/header/core/webhooks/APIQueryWebhookMsg.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/5/7. @@ -19,6 +20,7 @@ responseClass = APIQueryWebhookReply.class ) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryWebhookMsg extends APIQueryMessage { public static List __example__() { return Arrays.asList("name=test"); diff --git a/header/src/main/java/org/zstack/header/core/webhooks/APIUpdateWebhookMsg.java b/header/src/main/java/org/zstack/header/core/webhooks/APIUpdateWebhookMsg.java index 5fd7fa2ac67..d0e0ddbdde4 100755 --- a/header/src/main/java/org/zstack/header/core/webhooks/APIUpdateWebhookMsg.java +++ b/header/src/main/java/org/zstack/header/core/webhooks/APIUpdateWebhookMsg.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.MetadataImpact; /** * Created by xing5 on 2017/5/7. @@ -15,6 +16,7 @@ responseClass = APIUpdateWebhookEvent.class ) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateWebhookMsg extends APIMessage { @APIParam(resourceType = WebhookVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIAddHostMsg.java b/header/src/main/java/org/zstack/header/host/APIAddHostMsg.java index ad7c6dc9270..b50676d7c2d 100755 --- a/header/src/main/java/org/zstack/header/host/APIAddHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIAddHostMsg.java @@ -6,6 +6,9 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public abstract class APIAddHostMsg extends APICreateMessage implements AddHostMessage, APIAuditor { /** diff --git a/header/src/main/java/org/zstack/header/host/APIChangeHostStateMsg.java b/header/src/main/java/org/zstack/header/host/APIChangeHostStateMsg.java index e7afdd766fd..306d0aca3af 100755 --- a/header/src/main/java/org/zstack/header/host/APIChangeHostStateMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIChangeHostStateMsg.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.MetadataImpact; /** * @api change host state. When host state is Disabled, no vm can be created on this host. @@ -39,6 +40,7 @@ responseClass = APIChangeHostStateEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeHostStateMsg extends APIMessage implements HostMessage { /** * @desc host uuid diff --git a/header/src/main/java/org/zstack/header/host/APIDeleteHostMsg.java b/header/src/main/java/org/zstack/header/host/APIDeleteHostMsg.java index 89b2165358e..7c94de4721c 100755 --- a/header/src/main/java/org/zstack/header/host/APIDeleteHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIDeleteHostMsg.java @@ -11,6 +11,7 @@ import static org.zstack.header.message.APIDeleteMessage.DeletionMode.Permissive; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** @@ -44,6 +45,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteHostEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteHostMsg extends APIDeleteMessage implements HostMessage { /** * @desc host uuid diff --git a/header/src/main/java/org/zstack/header/host/APIGetHostBlockDevicesMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHostBlockDevicesMsg.java index b1976b37072..3f45a4c1c67 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetHostBlockDevicesMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHostBlockDevicesMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hosts/{uuid}/get-block-devices", method = HttpMethod.GET, responseClass = APIGetHostBlockDevicesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostBlockDevicesMsg extends APISyncCallMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIGetHostPowerStatusMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHostPowerStatusMsg.java index ab8b2f5760b..64f9111fb06 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetHostPowerStatusMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHostPowerStatusMsg.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.MetadataImpact; /** * @Author : jingwang @@ -15,6 +16,7 @@ responseClass = APIGetHostPowerStatusEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostPowerStatusMsg extends APIMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIGetHostSensorsMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHostSensorsMsg.java index d3a00973ea7..eee3208e282 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetHostSensorsMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHostSensorsMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hosts/{uuid}/get-sensors", method = HttpMethod.GET, responseClass = APIGetHostSensorsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostSensorsMsg extends APISyncCallMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIGetHostTaskMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHostTaskMsg.java index 2d10fad0f57..8b3e09ee405 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetHostTaskMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHostTaskMsg.java @@ -11,6 +11,7 @@ import java.util.function.Function; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2019/7/3. @@ -21,6 +22,7 @@ method = HttpMethod.GET, responseClass = APIGetChainTaskReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostTaskMsg extends APIGetChainTaskMsg { @APIParam(nonempty = true, resourceType = HostVO.class) private List hostUuids; diff --git a/header/src/main/java/org/zstack/header/host/APIGetHostWebSshUrlMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHostWebSshUrlMsg.java index 12415f71af6..5eee869f2d6 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetHostWebSshUrlMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHostWebSshUrlMsg.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.MetadataImpact; /** * @Author : jingwang @@ -16,6 +17,7 @@ responseClass = APIGetHostWebSshUrlEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostWebSshUrlMsg extends APIMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIGetHypervisorTypesMsg.java b/header/src/main/java/org/zstack/header/host/APIGetHypervisorTypesMsg.java index f758862bb77..fc15df1232b 100755 --- a/header/src/main/java/org/zstack/header/host/APIGetHypervisorTypesMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetHypervisorTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -31,6 +32,7 @@ method = HttpMethod.GET, responseClass = APIGetHypervisorTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHypervisorTypesMsg extends APISyncCallMessage { public static APIGetHypervisorTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/host/APIGetPhysicalMachineBlockDevicesMsg.java b/header/src/main/java/org/zstack/header/host/APIGetPhysicalMachineBlockDevicesMsg.java index 1de3303c7ac..945e626a672 100644 --- a/header/src/main/java/org/zstack/header/host/APIGetPhysicalMachineBlockDevicesMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIGetPhysicalMachineBlockDevicesMsg.java @@ -7,12 +7,14 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/host/get-block-devices", method = HttpMethod.GET, responseClass = APIGetPhysicalMachineBlockDevicesReply.class ) + @MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPhysicalMachineBlockDevicesMsg extends APISyncCallMessage { @APIParam(maxLength = 255) private String username; diff --git a/header/src/main/java/org/zstack/header/host/APIMountBlockDeviceMsg.java b/header/src/main/java/org/zstack/header/host/APIMountBlockDeviceMsg.java index 81c0f4c61f9..1a92a519c54 100644 --- a/header/src/main/java/org/zstack/header/host/APIMountBlockDeviceMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIMountBlockDeviceMsg.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.MetadataImpact; @RestRequest( path = "/host/mount-block-device", @@ -12,6 +13,7 @@ parameterName = "params", responseClass = APIMountBlockDeviceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIMountBlockDeviceMsg extends APIMessage { @APIParam(maxLength = 255) private String username; diff --git a/header/src/main/java/org/zstack/header/host/APIPowerOnHostMsg.java b/header/src/main/java/org/zstack/header/host/APIPowerOnHostMsg.java index f8f06ad1b33..9d49ac4e2e1 100644 --- a/header/src/main/java/org/zstack/header/host/APIPowerOnHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIPowerOnHostMsg.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.MetadataImpact; /** * @Author : jingwang @@ -15,6 +16,7 @@ responseClass = APIPowerOnHostEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIPowerOnHostMsg extends APIMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIPowerResetHostMsg.java b/header/src/main/java/org/zstack/header/host/APIPowerResetHostMsg.java index ef268974ef0..5f664dc4d67 100644 --- a/header/src/main/java/org/zstack/header/host/APIPowerResetHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIPowerResetHostMsg.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.MetadataImpact; /** * @Author : jingwang @@ -15,6 +16,7 @@ responseClass = APIPowerResetHostEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIPowerResetHostMsg extends APIMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIQueryHostMsg.java b/header/src/main/java/org/zstack/header/host/APIQueryHostMsg.java index f9f3be4e303..4c972082ce4 100755 --- a/header/src/main/java/org/zstack/header/host/APIQueryHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIQueryHostMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.rest.RestRequest; import static java.util.Arrays.asList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryHostReply.class, inventoryClass = HostInventory.class) @RestRequest( @@ -14,6 +15,7 @@ responseClass = APIQueryHostReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryHostMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/host/APIReconnectHostMsg.java b/header/src/main/java/org/zstack/header/host/APIReconnectHostMsg.java index b2149a22bd1..dde3bce9904 100755 --- a/header/src/main/java/org/zstack/header/host/APIReconnectHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIReconnectHostMsg.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.MetadataImpact; /** * @api reestablish connection to hypervisor agent @@ -37,6 +38,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectHostMsg extends APIMessage implements HostMessage { /** * @desc host uuid diff --git a/header/src/main/java/org/zstack/header/host/APIShutdownHostMsg.java b/header/src/main/java/org/zstack/header/host/APIShutdownHostMsg.java index 6c165a8b3aa..16b81a79a85 100644 --- a/header/src/main/java/org/zstack/header/host/APIShutdownHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIShutdownHostMsg.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.MetadataImpact; /** @@ -16,6 +17,7 @@ responseClass = APIShutdownHostEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIShutdownHostMsg extends APIMessage implements HostMessage { @APIParam(nonempty = true, resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIUpdateHostIpmiMsg.java b/header/src/main/java/org/zstack/header/host/APIUpdateHostIpmiMsg.java index 4494762e4a6..68d260c1098 100644 --- a/header/src/main/java/org/zstack/header/host/APIUpdateHostIpmiMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIUpdateHostIpmiMsg.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.MetadataImpact; /** * @Author : jingwang @@ -16,6 +17,7 @@ responseClass = APIUpdateHostIpmiEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateHostIpmiMsg extends APIMessage implements HostMessage { @APIParam(resourceType = HostEO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIUpdateHostMsg.java b/header/src/main/java/org/zstack/header/host/APIUpdateHostMsg.java index 6f4be4b485f..20481b758e7 100755 --- a/header/src/main/java/org/zstack/header/host/APIUpdateHostMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIUpdateHostMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ responseClass = APIUpdateHostEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateHostMsg extends APIMessage implements HostMessage { @APIParam(resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIUpdateHostNqnMsg.java b/header/src/main/java/org/zstack/header/host/APIUpdateHostNqnMsg.java index bcf87175376..648af259b67 100644 --- a/header/src/main/java/org/zstack/header/host/APIUpdateHostNqnMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIUpdateHostNqnMsg.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.MetadataImpact; @RestRequest( path = "/hosts/nqn/{uuid}/actions", @@ -11,6 +12,7 @@ responseClass = APIUpdateHostNqnEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateHostNqnMsg extends APIMessage implements HostMessage { @APIParam(resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/host/APIUpdateHostnameMsg.java b/header/src/main/java/org/zstack/header/host/APIUpdateHostnameMsg.java index 6ebef33152f..4139fdda67a 100644 --- a/header/src/main/java/org/zstack/header/host/APIUpdateHostnameMsg.java +++ b/header/src/main/java/org/zstack/header/host/APIUpdateHostnameMsg.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.MetadataImpact; @RestRequest( path = "/hosts/hostname/{uuid}/actions", @@ -11,6 +12,7 @@ responseClass = APIUpdateHostnameEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateHostnameMsg extends APIMessage implements HostMessage { @APIParam(resourceType = HostVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIChangeResourceOwnerMsg.java b/header/src/main/java/org/zstack/header/identity/APIChangeResourceOwnerMsg.java index 7ab98e17a11..94fc7a4ba17 100755 --- a/header/src/main/java/org/zstack/header/identity/APIChangeResourceOwnerMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIChangeResourceOwnerMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/4/16. @@ -15,6 +16,7 @@ responseClass = APIChangeResourceOwnerEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeResourceOwnerMsg extends APIMessage { @APIParam(resourceType = AccountVO.class) private String accountUuid; diff --git a/header/src/main/java/org/zstack/header/identity/APICreateAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APICreateAccountMsg.java index 602d6eaf359..8f18da8f882 100755 --- a/header/src/main/java/org/zstack/header/identity/APICreateAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APICreateAccountMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/accounts", @@ -17,6 +18,7 @@ responseClass = APICreateAccountEvent.class ) @EncryptionParamAllowed +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateAccountMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/identity/APIDeleteAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APIDeleteAccountMsg.java index 189ebc22869..044fb13b0cd 100755 --- a/header/src/main/java/org/zstack/header/identity/APIDeleteAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIDeleteAccountMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/15/2015. @@ -17,6 +18,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteAccountEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteAccountMsg extends APIDeleteMessage implements AccountMessage { @APIParam(resourceType = AccountVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIGetAccountQuotaUsageMsg.java b/header/src/main/java/org/zstack/header/identity/APIGetAccountQuotaUsageMsg.java index a404c57a9b5..5d6baa59bbf 100755 --- a/header/src/main/java/org/zstack/header/identity/APIGetAccountQuotaUsageMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIGetAccountQuotaUsageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 2/22/2016. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetAccountQuotaUsageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetAccountQuotaUsageMsg extends APISyncCallMessage implements AccountMessage { @APIParam(resourceType = AccountVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIGetResourceAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APIGetResourceAccountMsg.java index 40c0c35287f..b50bba409a1 100755 --- a/header/src/main/java/org/zstack/header/identity/APIGetResourceAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIGetResourceAccountMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/4/8. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIGetResourceAccountReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetResourceAccountMsg extends APISyncCallMessage { @APIParam(nonempty = true) private List resourceUuids; diff --git a/header/src/main/java/org/zstack/header/identity/APILogInByAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APILogInByAccountMsg.java index d9dac2ffdd3..bd0a75439a6 100755 --- a/header/src/main/java/org/zstack/header/identity/APILogInByAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APILogInByAccountMsg.java @@ -12,6 +12,7 @@ import org.zstack.header.rest.RestRequest; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; @SuppressCredentialCheck @RestRequest( @@ -21,6 +22,7 @@ responseClass = APILogInReply.class ) @EncryptionParamAllowed(actions = { EncryptionParamAllowed.ACTION_PUT_USER_INFO_INTO_SYSTEM_TAG }) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APILogInByAccountMsg extends APISessionMessage implements APILoginAuditor, APICaptchaMessage { @APIParam private String accountName; diff --git a/header/src/main/java/org/zstack/header/identity/APILogOutMsg.java b/header/src/main/java/org/zstack/header/identity/APILogOutMsg.java index 49fb79d1e02..939111f222d 100755 --- a/header/src/main/java/org/zstack/header/identity/APILogOutMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APILogOutMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.rest.RestRequest; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; @SuppressCredentialCheck @RestRequest( @@ -17,6 +18,7 @@ method = HttpMethod.DELETE, responseClass = APILogOutReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APILogOutMsg extends APISessionMessage implements APILoginAuditor { private String sessionUuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIQueryAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APIQueryAccountMsg.java index a66d41f6417..33618bbc867 100755 --- a/header/src/main/java/org/zstack/header/identity/APIQueryAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIQueryAccountMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/14/2015. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryAccountReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryAccountMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/identity/APIQueryAccountResourceRefMsg.java b/header/src/main/java/org/zstack/header/identity/APIQueryAccountResourceRefMsg.java index 611c616dcfc..342cb1a47f4 100755 --- a/header/src/main/java/org/zstack/header/identity/APIQueryAccountResourceRefMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIQueryAccountResourceRefMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 2/25/2016. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryAccountResourceRefReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryAccountResourceRefMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/identity/APIQueryQuotaMsg.java b/header/src/main/java/org/zstack/header/identity/APIQueryQuotaMsg.java index 8d3ba9ca852..97dcedfbfed 100755 --- a/header/src/main/java/org/zstack/header/identity/APIQueryQuotaMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIQueryQuotaMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/14/2015. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryQuotaReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryQuotaMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/identity/APIRenewSessionMsg.java b/header/src/main/java/org/zstack/header/identity/APIRenewSessionMsg.java index 24721f8215d..ee80bd3df8b 100644 --- a/header/src/main/java/org/zstack/header/identity/APIRenewSessionMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIRenewSessionMsg.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.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -17,6 +18,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRenewSessionMsg extends APIMessage { @APIParam(resourceType = SessionVO.class) private String sessionUuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIRevokeResourceSharingMsg.java b/header/src/main/java/org/zstack/header/identity/APIRevokeResourceSharingMsg.java index 87438fef066..625fea9c316 100755 --- a/header/src/main/java/org/zstack/header/identity/APIRevokeResourceSharingMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIRevokeResourceSharingMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/13/2015. @@ -19,6 +20,7 @@ isAction = true, responseClass = APIRevokeResourceSharingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRevokeResourceSharingMsg extends APIMessage { @APIParam(resourceType = ResourceVO.class, nonempty = true, scope = APIParam.SCOPE_MUST_OWNER) private List resourceUuids; diff --git a/header/src/main/java/org/zstack/header/identity/APIShareResourceMsg.java b/header/src/main/java/org/zstack/header/identity/APIShareResourceMsg.java index 5085e448422..4d0e2418c4c 100755 --- a/header/src/main/java/org/zstack/header/identity/APIShareResourceMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIShareResourceMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/13/2015. @@ -19,6 +20,7 @@ responseClass = APIShareResourceEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIShareResourceMsg extends APIMessage { @APIParam(resourceType = ResourceVO.class, nonempty = true, scope = APIParam.SCOPE_MUST_OWNER) private List resourceUuids; diff --git a/header/src/main/java/org/zstack/header/identity/APIUpdateAccountMsg.java b/header/src/main/java/org/zstack/header/identity/APIUpdateAccountMsg.java index 0364d3a38a6..d8411b593b0 100755 --- a/header/src/main/java/org/zstack/header/identity/APIUpdateAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIUpdateAccountMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import java.io.Serializable; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/accounts/{uuid}", @@ -16,6 +17,7 @@ responseClass = APIUpdateAccountEvent.class ) @EncryptionParamAllowed(forbiddenFields = {"uuid"}) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateAccountMsg extends APIMessage implements AccountMessage, Serializable { @APIParam(resourceType = AccountVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIUpdateQuotaMsg.java b/header/src/main/java/org/zstack/header/identity/APIUpdateQuotaMsg.java index d111fa4fbca..27f69732338 100755 --- a/header/src/main/java/org/zstack/header/identity/APIUpdateQuotaMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIUpdateQuotaMsg.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.MetadataImpact; /** * Created by frank on 7/14/2015. @@ -15,6 +16,7 @@ isAction = true, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateQuotaMsg extends APIMessage implements AccountMessage { @APIParam(resourceType = AccountVO.class) private String identityUuid; diff --git a/header/src/main/java/org/zstack/header/identity/APIValidateSessionMsg.java b/header/src/main/java/org/zstack/header/identity/APIValidateSessionMsg.java index 08f90e870b7..87fef141b44 100755 --- a/header/src/main/java/org/zstack/header/identity/APIValidateSessionMsg.java +++ b/header/src/main/java/org/zstack/header/identity/APIValidateSessionMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -16,6 +17,7 @@ responseClass = APIValidateSessionReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIValidateSessionMsg extends APISessionMessage { @APIParam private String sessionUuid; diff --git a/header/src/main/java/org/zstack/header/identity/login/APIGetLoginProceduresMsg.java b/header/src/main/java/org/zstack/header/identity/login/APIGetLoginProceduresMsg.java index 1968349f955..9619e6574c5 100644 --- a/header/src/main/java/org/zstack/header/identity/login/APIGetLoginProceduresMsg.java +++ b/header/src/main/java/org/zstack/header/identity/login/APIGetLoginProceduresMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/login/procedures", @@ -12,6 +13,7 @@ responseClass = APIGetLoginProceduresReply.class ) @SuppressCredentialCheck +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetLoginProceduresMsg extends APISyncCallMessage { @APIParam private String username; diff --git a/header/src/main/java/org/zstack/header/identity/login/APILogInMsg.java b/header/src/main/java/org/zstack/header/identity/login/APILogInMsg.java index c1e8ee7254a..a8ef0cab468 100644 --- a/header/src/main/java/org/zstack/header/identity/login/APILogInMsg.java +++ b/header/src/main/java/org/zstack/header/identity/login/APILogInMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.rest.RestRequest; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; @SuppressCredentialCheck @RestRequest( @@ -19,6 +20,7 @@ method = HttpMethod.PUT, responseClass = APILogInReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APILogInMsg extends APISessionMessage implements APILoginAuditor { @APIParam private String username; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIAttachRoleToAccountMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIAttachRoleToAccountMsg.java index 4bbc3a1b4cb..49cde266c83 100755 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIAttachRoleToAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIAttachRoleToAccountMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/identities/accounts/{accountUuid}/roles/{roleUuid}", @@ -13,6 +14,7 @@ parameterName = "params", responseClass = APIAttachRoleToAccountEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachRoleToAccountMsg extends APIMessage implements RoleMessage { @APIParam(resourceType = RoleVO.class) private String roleUuid; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APICreateRoleMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APICreateRoleMsg.java index 68728eda20b..0bd44d903d3 100755 --- a/header/src/main/java/org/zstack/header/identity/role/api/APICreateRoleMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APICreateRoleMsg.java @@ -9,8 +9,10 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/identities/roles", method = HttpMethod.POST, responseClass = APICreateRoleEvent.class, parameterName = "params") +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateRoleMsg extends APICreateMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIDeleteRoleMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIDeleteRoleMsg.java index fa98c27bf66..3d493196fa7 100755 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIDeleteRoleMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIDeleteRoleMsg.java @@ -5,8 +5,10 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/identities/roles/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteRoleEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteRoleMsg extends APIDeleteMessage implements RoleMessage { @APIParam(resourceType = RoleVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIDetachRoleFromAccountMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIDetachRoleFromAccountMsg.java index 7303a96f4d8..222bd1b5f7e 100755 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIDetachRoleFromAccountMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIDetachRoleFromAccountMsg.java @@ -4,8 +4,10 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/identities/accounts/{accountUuid}/roles/{roleUuid}", method = HttpMethod.DELETE, responseClass = APIDetachRoleFromAccountEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachRoleFromAccountMsg extends APIDeleteMessage implements RoleMessage { @APIParam private String roleUuid; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIGetRolePolicyActionsMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIGetRolePolicyActionsMsg.java index 478e8c80943..fa7c8868adf 100644 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIGetRolePolicyActionsMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIGetRolePolicyActionsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by Wenhao.Zhang on 2024/08/30 @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetRolePolicyActionsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetRolePolicyActionsMsg extends APISyncCallMessage { @APIParam(required = false) private boolean showAllPolicies; diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleAccountRefMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleAccountRefMsg.java index 101aa58c20a..c99e12d7207 100644 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleAccountRefMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleAccountRefMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryRoleAccountRefReply.class, inventoryClass = RoleAccountRefInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryRoleAccountRefReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryRoleAccountRefMsg extends APIQueryMessage { public static List __example__() { return list("roleUuid=686cb963323e491e955a0fd0b49dd743"); diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleMsg.java index 163696932e9..a6275e09e8d 100755 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIQueryRoleMsg.java @@ -9,10 +9,12 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryRoleReply.class, inventoryClass = RoleInventory.class) @RestRequest(path = "/identities/roles", optionalPaths = {"/identities/roles/{uuid}"}, method = HttpMethod.GET, responseClass = APIQueryRoleReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryRoleMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/identity/role/api/APIUpdateRoleMsg.java b/header/src/main/java/org/zstack/header/identity/role/api/APIUpdateRoleMsg.java index fac8534b0ef..058954cd258 100644 --- a/header/src/main/java/org/zstack/header/identity/role/api/APIUpdateRoleMsg.java +++ b/header/src/main/java/org/zstack/header/identity/role/api/APIUpdateRoleMsg.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by kayo on 2018/7/26. @@ -18,6 +19,7 @@ method = HttpMethod.PUT, isAction = true, responseClass = APIUpdateRoleEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateRoleMsg extends APIMessage implements RoleMessage { @APIParam(resourceType = RoleVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/image/APIAddImageMsg.java b/header/src/main/java/org/zstack/header/image/APIAddImageMsg.java index 2bdec876546..a6b72e0fbee 100755 --- a/header/src/main/java/org/zstack/header/image/APIAddImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIAddImageMsg.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(ImageVO.class) @RestRequest( @@ -21,6 +22,7 @@ responseClass = APIAddImageEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddImageMsg extends APICreateMessage implements APIAuditor, AddImageMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/image/APICalculateImageHashMsg.java b/header/src/main/java/org/zstack/header/image/APICalculateImageHashMsg.java index 05c0a3f47ae..8470bcbc5d4 100644 --- a/header/src/main/java/org/zstack/header/image/APICalculateImageHashMsg.java +++ b/header/src/main/java/org/zstack/header/image/APICalculateImageHashMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.BackupStorageVO; import org.zstack.header.storage.backup.ImageHashAlgorithm; +import org.zstack.header.vm.MetadataImpact; /** * @ Author : yh.w @@ -19,6 +20,7 @@ responseClass = APICalculateImageHashEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICalculateImageHashMsg extends APIMessage implements ImageMessage, APIAuditor { @APIParam(resourceType = ImageVO.class) diff --git a/header/src/main/java/org/zstack/header/image/APIChangeImageStateMsg.java b/header/src/main/java/org/zstack/header/image/APIChangeImageStateMsg.java index 35d93311344..9cf99b966a6 100755 --- a/header/src/main/java/org/zstack/header/image/APIChangeImageStateMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIChangeImageStateMsg.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.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -17,6 +18,7 @@ responseClass = APIChangeImageStateEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeImageStateMsg extends APIMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeMsg.java b/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeMsg.java index f65c1d1474a..570fb260705 100755 --- a/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -22,6 +23,7 @@ ) @TagResourceType(ImageVO.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDataVolumeTemplateFromVolumeMsg extends APICreateMessage implements APIAuditor, CreateDataVolumeTemplateMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeSnapshotMsg.java index 92a98d0fcf2..bb68d75515b 100755 --- a/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/image/APICreateDataVolumeTemplateFromVolumeSnapshotMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -22,6 +23,7 @@ ) @TagResourceType(ImageVO.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDataVolumeTemplateFromVolumeSnapshotMsg extends APICreateMessage implements APIAuditor, CreateDataVolumeTemplateMessage { @APIParam(resourceType = VolumeSnapshotVO.class) private String snapshotUuid; diff --git a/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromRootVolumeMsg.java b/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromRootVolumeMsg.java index 3d26025a76c..90740fcb946 100755 --- a/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromRootVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromRootVolumeMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/images/root-volume-templates/from/volumes/{rootVolumeUuid}", @@ -20,6 +21,7 @@ ) @TagResourceType(ImageVO.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateRootVolumeTemplateFromRootVolumeMsg extends APICreateMessage implements APIAuditor, CreateRootVolumeTemplateMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromVolumeSnapshotMsg.java index 2263cac9c11..88a7e0a663d 100755 --- a/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/image/APICreateRootVolumeTemplateFromVolumeSnapshotMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -22,6 +23,7 @@ ) @TagResourceType(ImageVO.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateRootVolumeTemplateFromVolumeSnapshotMsg extends APICreateMessage implements APIAuditor, CreateRootVolumeTemplateMessage { @APIParam(resourceType = VolumeSnapshotVO.class) private String snapshotUuid; diff --git a/header/src/main/java/org/zstack/header/image/APIDeleteImageMsg.java b/header/src/main/java/org/zstack/header/image/APIDeleteImageMsg.java index b3c24b4d1f9..4b9f9cef185 100755 --- a/header/src/main/java/org/zstack/header/image/APIDeleteImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIDeleteImageMsg.java @@ -10,12 +10,14 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/images/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteImageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteImageMsg extends APIDeleteMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/image/APIExpungeImageMsg.java b/header/src/main/java/org/zstack/header/image/APIExpungeImageMsg.java index b8433e3e449..dba338d762f 100755 --- a/header/src/main/java/org/zstack/header/image/APIExpungeImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIExpungeImageMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/15/2015. @@ -18,6 +19,7 @@ responseClass = APIExpungeImageEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIExpungeImageMsg extends APIMessage implements ImageMessage { @APIParam(required = false, resourceType = ImageVO.class) // used for cloudformation diff --git a/header/src/main/java/org/zstack/header/image/APIGetCandidateBackupStorageForCreatingImageMsg.java b/header/src/main/java/org/zstack/header/image/APIGetCandidateBackupStorageForCreatingImageMsg.java index 843d5e0d1c0..36d216ec074 100755 --- a/header/src/main/java/org/zstack/header/image/APIGetCandidateBackupStorageForCreatingImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIGetCandidateBackupStorageForCreatingImageMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.SDK; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.volume.VolumeVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/8/30. @@ -28,6 +29,7 @@ "GetBackupStorageForCreatingImageFromVolumeSnapshot=/images/volume-snapshots/{volumeSnapshotUuid}/candidate-backup-storage", } ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateBackupStorageForCreatingImageMsg extends APISyncCallMessage { @APIParam(resourceType = VolumeVO.class, required = false) private String volumeUuid; diff --git a/header/src/main/java/org/zstack/header/image/APIGetCandidateImagesForCreatingVmMsg.java b/header/src/main/java/org/zstack/header/image/APIGetCandidateImagesForCreatingVmMsg.java index 7c0ed994164..459a636dcc7 100644 --- a/header/src/main/java/org/zstack/header/image/APIGetCandidateImagesForCreatingVmMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIGetCandidateImagesForCreatingVmMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateImagesForCreatingVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateImagesForCreatingVmMsg extends APISyncCallMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String primaryStorageUuid; diff --git a/header/src/main/java/org/zstack/header/image/APIGetUploadImageJobDetailsMsg.java b/header/src/main/java/org/zstack/header/image/APIGetUploadImageJobDetailsMsg.java index aff353fc06c..2b00c47d1c9 100644 --- a/header/src/main/java/org/zstack/header/image/APIGetUploadImageJobDetailsMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIGetUploadImageJobDetailsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2021/3/29. @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetUploadImageJobDetailsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetUploadImageJobDetailsMsg extends APISyncCallMessage { @APIParam private String imageId; diff --git a/header/src/main/java/org/zstack/header/image/APIQueryImageMsg.java b/header/src/main/java/org/zstack/header/image/APIQueryImageMsg.java index 1e0d6c1a591..1c112210c8e 100755 --- a/header/src/main/java/org/zstack/header/image/APIQueryImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIQueryImageMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryImageReply.class, inventoryClass = ImageInventory.class) @RestRequest( @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryImageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryImageMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/image/APIRecoverImageMsg.java b/header/src/main/java/org/zstack/header/image/APIRecoverImageMsg.java index 7e20a04a6d8..6a247f08d8b 100755 --- a/header/src/main/java/org/zstack/header/image/APIRecoverImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIRecoverImageMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/15/2015. @@ -17,6 +18,7 @@ responseClass = APIRecoverImageEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRecoverImageMsg extends APIMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class) private String imageUuid; diff --git a/header/src/main/java/org/zstack/header/image/APISetImageBootModeMsg.java b/header/src/main/java/org/zstack/header/image/APISetImageBootModeMsg.java index 7180f30a25a..e694d69c213 100644 --- a/header/src/main/java/org/zstack/header/image/APISetImageBootModeMsg.java +++ b/header/src/main/java/org/zstack/header/image/APISetImageBootModeMsg.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.MetadataImpact; @RestRequest( path = "/images/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetImageBootModeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISetImageBootModeMsg extends APIMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/image/APISyncImageSizeMsg.java b/header/src/main/java/org/zstack/header/image/APISyncImageSizeMsg.java index 1ee695c66c7..c568d99503e 100755 --- a/header/src/main/java/org/zstack/header/image/APISyncImageSizeMsg.java +++ b/header/src/main/java/org/zstack/header/image/APISyncImageSizeMsg.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.MetadataImpact; /** * Created by xing5 on 2016/5/6. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISyncImageSizeMsg extends APIMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/image/APIUpdateImageMsg.java b/header/src/main/java/org/zstack/header/image/APIUpdateImageMsg.java index 63763065ab2..922c7d0408a 100755 --- a/header/src/main/java/org/zstack/header/image/APIUpdateImageMsg.java +++ b/header/src/main/java/org/zstack/header/image/APIUpdateImageMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdateImageEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateImageMsg extends APIMessage implements ImageMessage { @APIParam(resourceType = ImageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APICancelLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APICancelLongJobMsg.java index 1f142f73d61..478454c619a 100644 --- a/header/src/main/java/org/zstack/header/longjob/APICancelLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APICancelLongJobMsg.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.MetadataImpact; /** * Created by GuoYi on 11/13/17. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APICancelLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICancelLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APICleanLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APICleanLongJobMsg.java index c4639c96a7c..33db15a9c92 100644 --- a/header/src/main/java/org/zstack/header/longjob/APICleanLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APICleanLongJobMsg.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.MetadataImpact; /** * Created by wushan on 8/23/21 @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APICleanLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APIDeleteLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APIDeleteLongJobMsg.java index 1f05fafcca6..4707fdddb82 100644 --- a/header/src/main/java/org/zstack/header/longjob/APIDeleteLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APIDeleteLongJobMsg.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.MetadataImpact; /** * Created by GuoYi on 12/7/17. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, successIfResourceNotExisting = true, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APIQueryLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APIQueryLongJobMsg.java index 87b8f926f77..e37bb61bdae 100644 --- a/header/src/main/java/org/zstack/header/longjob/APIQueryLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APIQueryLongJobMsg.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by GuoYi on 11/13/17. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryLongJobReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLongJobMsg extends APIQueryMessage { public static List __example__() { return Arrays.asList(); diff --git a/header/src/main/java/org/zstack/header/longjob/APIRerunLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APIRerunLongJobMsg.java index 84001f30ffc..f2396f9a5ef 100644 --- a/header/src/main/java/org/zstack/header/longjob/APIRerunLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APIRerunLongJobMsg.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.MetadataImpact; /** * Created by GuoYi on 11/13/17. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIRerunLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRerunLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APIResumeLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APIResumeLongJobMsg.java index ac93b63a820..de6c6e7cb11 100644 --- a/header/src/main/java/org/zstack/header/longjob/APIResumeLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APIResumeLongJobMsg.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.MetadataImpact; @RestRequest( path = "/longjobs/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APIResumeLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIResumeLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/longjob/APISubmitLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APISubmitLongJobMsg.java index 4c2c82f3345..cae7c2a44f6 100644 --- a/header/src/main/java/org/zstack/header/longjob/APISubmitLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APISubmitLongJobMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.tag.TagResourceType; import org.zstack.header.vo.ResourceVO; import org.zstack.utils.DebugUtils; +import org.zstack.header.vm.MetadataImpact; /** * Created by GuoYi on 11/13/17. @@ -22,6 +23,7 @@ parameterName = "params", responseClass = APISubmitLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISubmitLongJobMsg extends APICreateMessage implements APILongJobAuditor { @APIParam(maxLength = 255, required = false) private String name; diff --git a/header/src/main/java/org/zstack/header/longjob/APIUpdateLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APIUpdateLongJobMsg.java index 1ff5c0f5495..ed64c0295b6 100644 --- a/header/src/main/java/org/zstack/header/longjob/APIUpdateLongJobMsg.java +++ b/header/src/main/java/org/zstack/header/longjob/APIUpdateLongJobMsg.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.MetadataImpact; /** * * Created on 2/3/2020 @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateLongJobEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateLongJobMsg extends APIMessage implements LongJobMessage { @APIParam(resourceType = LongJobVO.class, scope = APIParam.SCOPE_MUST_OWNER) private String uuid; diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetCurrentTimeMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetCurrentTimeMsg.java index ec569b5d0ba..4aa623a4cf6 100755 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetCurrentTimeMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetCurrentTimeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by Mei Lei on 11/1/16. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APIGetCurrentTimeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCurrentTimeMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetCurrentTimeMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeArchMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeArchMsg.java index 2a928050d64..751656870da 100644 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeArchMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeArchMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** @@ -17,6 +18,7 @@ responseClass = APIGetManagementNodeArchReply.class ) @SuppressCredentialCheck +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetManagementNodeArchMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetManagementNodeArchMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeOSMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeOSMsg.java index 407d63b3de6..948751d8ae3 100644 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeOSMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetManagementNodeOSMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by Jialong on 2021/03/15. @@ -16,6 +17,7 @@ responseClass = APIGetManagementNodeOSReply.class ) @SuppressCredentialCheck +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetManagementNodeOSMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetManagementNodeOSMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetPlatformTimeZoneMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetPlatformTimeZoneMsg.java index 8e93957d06f..38f3344577a 100644 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetPlatformTimeZoneMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetPlatformTimeZoneMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by Qi Le on 2021/3/15 @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetPlatformTimeZoneReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPlatformTimeZoneMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetPlatformTimeZoneMsg __example__() { APIGetPlatformTimeZoneMsg msg = new APIGetPlatformTimeZoneMsg(); diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetSupportAPIsMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetSupportAPIsMsg.java index 7ff8adb7870..c0af6aa3184 100644 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetSupportAPIsMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetSupportAPIsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @SuppressCredentialCheck @RestRequest( @@ -12,6 +13,7 @@ method = HttpMethod.PUT, responseClass = APIGetSupportAPIsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetSupportAPIsMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetSupportAPIsMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/managementnode/APIGetVersionMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIGetVersionMsg.java index f9a9f3209d6..897ef2f16ad 100755 --- a/header/src/main/java/org/zstack/header/managementnode/APIGetVersionMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIGetVersionMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.identity.SuppressCredentialCheck; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/14/2015. @@ -15,6 +16,7 @@ responseClass = APIGetVersionReply.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVersionMsg extends APISyncCallMessage implements APIManagementNodeMessage { public static APIGetVersionMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/managementnode/APIQueryManagementNodeMsg.java b/header/src/main/java/org/zstack/header/managementnode/APIQueryManagementNodeMsg.java index c0676c41b5d..cfc6c4895b3 100755 --- a/header/src/main/java/org/zstack/header/managementnode/APIQueryManagementNodeMsg.java +++ b/header/src/main/java/org/zstack/header/managementnode/APIQueryManagementNodeMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryManagementNodeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryManagementNodeMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToClusterMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToClusterMsg.java index d2581c6d2dd..3191910e844 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToClusterMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToClusterMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api attach l2Network to cluster @@ -41,6 +42,7 @@ responseClass = APIAttachL2NetworkToClusterEvent.class, parameterName = "null" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachL2NetworkToClusterMsg extends APIMessage implements L2NetworkMessage { /** * @desc l2Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToHostMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToHostMsg.java index ce44f20792e..757f99b7b4f 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToHostMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIAttachL2NetworkToHostMsg.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.MetadataImpact; @RestRequest( path = "/l2-networks/{l2NetworkUuid}/hosts/{hostUuid}", @@ -12,6 +13,7 @@ responseClass = APIAttachL2NetworkToHostEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachL2NetworkToHostMsg extends APIMessage implements L2NetworkMessage { /** * @desc l2Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l2/APICreateL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APICreateL2NetworkMsg.java index 78eaf161d95..1f9de5322a6 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APICreateL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APICreateL2NetworkMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; /** * @api create l2Network @@ -43,6 +44,8 @@ * @since 0.1.0 */ +@MetadataImpact(MetadataImpact.Impact.NONE) + public abstract class APICreateL2NetworkMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/header/src/main/java/org/zstack/header/network/l2/APICreateL2NoVlanNetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APICreateL2NoVlanNetworkMsg.java index 1da72c4ef51..e2455b19a2b 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APICreateL2NoVlanNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APICreateL2NoVlanNetworkMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -13,6 +14,7 @@ responseClass = APICreateL2NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2NoVlanNetworkMsg extends APICreateL2NetworkMsg { @Override public String getType() { diff --git a/header/src/main/java/org/zstack/header/network/l2/APICreateL2VlanNetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APICreateL2VlanNetworkMsg.java index 7439ac800df..eb6ad8fb5cc 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APICreateL2VlanNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APICreateL2VlanNetworkMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * @api create a l2VlanNetwork @@ -48,6 +49,7 @@ responseClass = APICreateL2VlanNetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2VlanNetworkMsg extends APICreateL2NetworkMsg { /** * @desc vlan id diff --git a/header/src/main/java/org/zstack/header/network/l2/APIDeleteL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIDeleteL2NetworkMsg.java index b576aae570a..781116501ed 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIDeleteL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIDeleteL2NetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api delete l2Network @@ -42,6 +43,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteL2NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteL2NetworkMsg extends APIDeleteMessage implements L2NetworkMessage { /** * @desc l2Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromClusterMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromClusterMsg.java index 4d03c4249e7..5a661faf4f6 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromClusterMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromClusterMsg.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.MetadataImpact; /** * @api detach l2Network from a cluster @@ -39,6 +40,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachL2NetworkFromClusterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachL2NetworkFromClusterMsg extends APIMessage implements L2NetworkMessage { /** * @desc l2Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromHostMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromHostMsg.java index c07865a3b4c..609c234f25f 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromHostMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIDetachL2NetworkFromHostMsg.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.MetadataImpact; @RestRequest( path = "/l2-networks/{l2NetworkUuid}/hosts/{hostUuid}", method = HttpMethod.DELETE, responseClass = APIDetachL2NetworkFromHostEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachL2NetworkFromHostMsg extends APIMessage implements L2NetworkMessage { /** * @desc l2Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateClustersForAttachingL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateClustersForAttachingL2NetworkMsg.java index 7e0e8c92093..fbf07e93e77 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateClustersForAttachingL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateClustersForAttachingL2NetworkMsg.java @@ -6,12 +6,14 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/l2-networks/{l2NetworkUuid}/cluster-candidates", method = HttpMethod.GET, responseClass = APIGetCandidateClustersForAttachingL2NetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateClustersForAttachingL2NetworkMsg extends APIGetMessage { @APIParam(resourceType = L2NetworkVO.class) private String l2NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateL2NetworksForAttachingClusterMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateL2NetworksForAttachingClusterMsg.java index 39a42ff317a..35102b28f29 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateL2NetworksForAttachingClusterMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIGetCandidateL2NetworksForAttachingClusterMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APIGetMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/cluster/{clusterUuid}/l2-candidates", method = HttpMethod.GET, responseClass = APIGetCandidateL2NetworksForAttachingClusterReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateL2NetworksForAttachingClusterMsg extends APIGetMessage { @APIParam(resourceType = ClusterVO.class) private String clusterUuid; diff --git a/header/src/main/java/org/zstack/header/network/l2/APIGetL2NetworkTypesMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIGetL2NetworkTypesMsg.java index 1c0f412e6d8..c0911b3057a 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIGetL2NetworkTypesMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIGetL2NetworkTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get supported l2Network types @@ -33,6 +34,7 @@ method = HttpMethod.GET, responseClass = APIGetL2NetworkTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL2NetworkTypesMsg extends APISyncCallMessage { public static APIGetL2NetworkTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l2/APIGetVSwitchTypesMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIGetVSwitchTypesMsg.java index 52cbf2557ec..a72165d3585 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIGetVSwitchTypesMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIGetVSwitchTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get supported vSwitch types @@ -34,6 +35,7 @@ method = HttpMethod.GET, responseClass = APIGetVSwitchTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVSwitchTypesMsg extends APISyncCallMessage { public static APIGetVSwitchTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l2/APIQueryL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIQueryL2NetworkMsg.java index 52a59aeda64..c85fad706a5 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIQueryL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIQueryL2NetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryL2NetworkReply.class, inventoryClass = L2NetworkInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryL2NetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryL2NetworkMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/network/l2/APIQueryL2VlanNetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIQueryL2VlanNetworkMsg.java index 0c1829b6e77..c91ead4f267 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIQueryL2VlanNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIQueryL2VlanNetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryL2VlanNetworkReply.class, inventoryClass = L2VlanNetworkInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryL2VlanNetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryL2VlanNetworkMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkMsg.java index 55a7da17e32..20910fc6cf3 100755 --- a/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdateL2NetworkEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateL2NetworkMsg extends APIMessage implements L2NetworkMessage { @APIParam(resourceType = L2NetworkVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkVirtualNetworkIdMsg.java b/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkVirtualNetworkIdMsg.java index 841dc4707f1..05462d7016a 100644 --- a/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkVirtualNetworkIdMsg.java +++ b/header/src/main/java/org/zstack/header/network/l2/APIUpdateL2NetworkVirtualNetworkIdMsg.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.MetadataImpact; /** * Created by boce.wang on 03/20/2024. @@ -16,6 +17,7 @@ responseClass = APIUpdateL2NetworkVirtualNetworkIdEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateL2NetworkVirtualNetworkIdMsg extends APIMessage implements L2NetworkMessage, APIAuditor { @APIParam(resourceType = L2NetworkVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddDnsToL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddDnsToL3NetworkMsg.java index 3b1ba12a2db..611fb6b4dd5 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddDnsToL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddDnsToL3NetworkMsg.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.MetadataImpact; /** * @api add dns to L3Network @@ -39,6 +40,7 @@ responseClass = APIAddDnsToL3NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddDnsToL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddHostRouteToL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddHostRouteToL3NetworkMsg.java index 2d986f5d5ed..30532a3ca44 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddHostRouteToL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddHostRouteToL3NetworkMsg.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.MetadataImpact; @RestRequest( path = "/l3-networks/{l3NetworkUuid}/hostroute", @@ -11,6 +12,7 @@ responseClass = APIAddHostRouteToL3NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddHostRouteToL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeByNetworkCidrMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeByNetworkCidrMsg.java index 366addbc921..4fa8682877c 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeByNetworkCidrMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeByNetworkCidrMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -15,6 +16,7 @@ parameterName = "params", responseClass = APIAddIpRangeByNetworkCidrEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddIpRangeByNetworkCidrMsg extends APICreateMessage implements L3NetworkMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeMsg.java index bb85b5526bc..01a5028a073 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddIpRangeMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * @api add a ip range to l3Network @@ -54,6 +55,7 @@ responseClass = APIAddIpRangeEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddIpRangeMsg extends APICreateMessage implements L3NetworkMessage, APIAuditor { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeByNetworkCidrMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeByNetworkCidrMsg.java index ec5adc0f36e..207119d3b94 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeByNetworkCidrMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeByNetworkCidrMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; import org.zstack.utils.network.IPv6Constants; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -16,6 +17,7 @@ parameterName = "params", responseClass = APIAddIpRangeByNetworkCidrEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddIpv6RangeByNetworkCidrMsg extends APICreateMessage implements L3NetworkMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeMsg.java index 155227c27d4..f1d4520acf8 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddIpv6RangeMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; import org.zstack.utils.network.IPv6Constants; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(L3NetworkVO.class) @RestRequest( @@ -17,6 +18,7 @@ responseClass = APIAddIpRangeEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddIpv6RangeMsg extends APICreateMessage implements L3NetworkMessage, APIAuditor { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIAddReservedIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIAddReservedIpRangeMsg.java index a342ba15459..d043efc0960 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIAddReservedIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIAddReservedIpRangeMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(L3NetworkVO.class) @Action(category = L3NetworkConstant.ACTION_CATEGORY) @@ -18,6 +19,7 @@ responseClass = APIAddReservedIpRangeEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddReservedIpRangeMsg extends APICreateMessage implements L3NetworkMessage, APIAuditor { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIChangeL3NetworkStateMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIChangeL3NetworkStateMsg.java index a91776cecb3..cf6eb1cdcc7 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIChangeL3NetworkStateMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIChangeL3NetworkStateMsg.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.MetadataImpact; /** * @api change l3Network state @@ -39,6 +40,7 @@ responseClass = APIChangeL3NetworkStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeL3NetworkStateMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APICheckIpAvailabilityMsg.java b/header/src/main/java/org/zstack/header/network/l3/APICheckIpAvailabilityMsg.java index a102e1058a5..e0245ca5d7e 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APICheckIpAvailabilityMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APICheckIpAvailabilityMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 1/21/2016. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APICheckIpAvailabilityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckIpAvailabilityMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APICreateL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APICreateL3NetworkMsg.java index eadac34cb1b..a39a217bfe8 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APICreateL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APICreateL3NetworkMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * @api create l3Network @@ -49,6 +50,7 @@ responseClass = APICreateL3NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL3NetworkMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpAddressMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpAddressMsg.java index bc498f9cf68..cad7f4e2b8d 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpAddressMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpAddressMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(L3NetworkVO.class) @Action(category = L3NetworkConstant.ACTION_CATEGORY) @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteIpAddressEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteIpAddressMsg extends APIDeleteMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpRangeMsg.java index 036bd8ee1b3..cce6bf63255 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIDeleteIpRangeMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api delete ip range @@ -43,6 +44,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteIpRangeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteIpRangeMsg extends APIDeleteMessage implements L3NetworkMessage, IpRangeMessage { /** * @desc ip range uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIDeleteL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIDeleteL3NetworkMsg.java index 30df1f4c42c..5323139a552 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIDeleteL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIDeleteL3NetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api delete l3Network @@ -42,6 +43,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteL3NetworkMsg extends APIDeleteMessage implements L3NetworkMessage { /** * @desc l3NetworkUuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIDeleteReservedIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIDeleteReservedIpRangeMsg.java index 7851156b421..1255c43edba 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIDeleteReservedIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIDeleteReservedIpRangeMsg.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.MetadataImpact; @Action(category = L3NetworkConstant.ACTION_CATEGORY) @RestRequest( @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteReservedIpRangeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteReservedIpRangeMsg extends APIDeleteMessage implements L3NetworkMessage, IpRangeMessage { /** * @desc ip range uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIGetFreeIpMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIGetFreeIpMsg.java index a4197687f51..0cf0fbb4131 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIGetFreeIpMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIGetFreeIpMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.rest.SDK; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -26,6 +27,7 @@ "GetFreeIpOfIpRange=/l3-networks/ip-ranges/{ipRangeUuid}/ip/free" } ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetFreeIpMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class, required = false) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIGetIpAddressCapacityMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIGetIpAddressCapacityMsg.java index 05476e4437b..1052e9ad847 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIGetIpAddressCapacityMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIGetIpAddressCapacityMsg.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIGetIpAddressCapacityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetIpAddressCapacityMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = ZoneVO.class) private List zoneUuids; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkMtuMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkMtuMsg.java index 0c3e5d4cdaa..f757b44957c 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkMtuMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkMtuMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 19/05/2017. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetL3NetworkMtuReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL3NetworkMtuMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkRouterInterfaceIpMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkRouterInterfaceIpMsg.java index 7d0ee053fdb..c119f5aa96a 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkRouterInterfaceIpMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkRouterInterfaceIpMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/l3-networks/{l3NetworkUuid}/router-interface-ip", method = HttpMethod.GET, responseClass = APIGetL3NetworkRouterInterfaceIpReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL3NetworkRouterInterfaceIpMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkTypesMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkTypesMsg.java index ce63649c8bf..dc3fd5fcb61 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkTypesMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIGetL3NetworkTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIGetL3NetworkTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL3NetworkTypesMsg extends APISyncCallMessage { public static APIGetL3NetworkTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l3/APIQueryAddressPoolMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIQueryAddressPoolMsg.java index 65ae6810aaa..43e8cbc2db2 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APIQueryAddressPoolMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIQueryAddressPoolMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryAddressPoolReply.class, inventoryClass = AddressPoolInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryAddressPoolReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryAddressPoolMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l3/APIQueryIpAddressMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIQueryIpAddressMsg.java index d0949bfb1e4..516746b2839 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIQueryIpAddressMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIQueryIpAddressMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryIpAddressReply.class, inventoryClass = UsedIpInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryIpAddressReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryIpAddressMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/network/l3/APIQueryIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIQueryIpRangeMsg.java index dfbafbe6763..c801f168096 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIQueryIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIQueryIpRangeMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryIpRangeReply.class, inventoryClass = IpRangeInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryIpRangeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryIpRangeMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/network/l3/APIQueryL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIQueryL3NetworkMsg.java index ff164c0e352..669de7c2ec8 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIQueryL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIQueryL3NetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryL3NetworkReply.class, inventoryClass = L3NetworkInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryL3NetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryL3NetworkMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/network/l3/APIRemoveDnsFromL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIRemoveDnsFromL3NetworkMsg.java index ef891a56af0..7943c33601c 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIRemoveDnsFromL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIRemoveDnsFromL3NetworkMsg.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.MetadataImpact; /** * @api remove dns from l3Network @@ -38,6 +39,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveDnsFromL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveDnsFromL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APIRemoveHostRouteFromL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIRemoveHostRouteFromL3NetworkMsg.java index 7a9a33bc70c..6d0620aac47 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIRemoveHostRouteFromL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIRemoveHostRouteFromL3NetworkMsg.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.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveHostRouteFromL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveHostRouteFromL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkMtuMsg.java b/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkMtuMsg.java index bb2fea2fe11..e0c9e15c79c 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkMtuMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkMtuMsg.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.MetadataImpact; /** * Created by weiwang on 18/05/2017. @@ -14,6 +15,7 @@ responseClass = APISetL3NetworkMtuEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISetL3NetworkMtuMsg extends APIMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkRouterInterfaceIpMsg.java b/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkRouterInterfaceIpMsg.java index 33f65b20d70..d8c14daab41 100644 --- a/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkRouterInterfaceIpMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APISetL3NetworkRouterInterfaceIpMsg.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.MetadataImpact; @RestRequest( path = "/l3-networks/{l3NetworkUuid}/router-interface-ip", @@ -11,6 +12,7 @@ responseClass = APISetL3NetworkRouterInterfaceIpEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISetL3NetworkRouterInterfaceIpMsg extends APIMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIUpdateIpRangeMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIUpdateIpRangeMsg.java index 91cae40d23e..c2144d6e436 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIUpdateIpRangeMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIUpdateIpRangeMsg.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.MetadataImpact; /** * Created by frank on 6/16/2015. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateIpRangeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateIpRangeMsg extends APIMessage implements L3NetworkMessage, IpRangeMessage { @APIParam(resourceType = IpRangeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/network/l3/APIUpdateL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/l3/APIUpdateL3NetworkMsg.java index 40eb10f3c4c..0ac3bdebcff 100755 --- a/header/src/main/java/org/zstack/header/network/l3/APIUpdateL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/APIUpdateL3NetworkMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateL3NetworkMsg extends APIMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/network/service/APIAddNetworkServiceProviderMsg.java b/header/src/main/java/org/zstack/header/network/service/APIAddNetworkServiceProviderMsg.java index 035091c4f14..737fbb26fc2 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIAddNetworkServiceProviderMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIAddNetworkServiceProviderMsg.java @@ -4,6 +4,9 @@ import org.zstack.header.message.APIParam; import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddNetworkServiceProviderMsg extends APIMessage { @APIParam diff --git a/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceProviderToL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceProviderToL2NetworkMsg.java index 82fe29b08b0..62d2c701ee5 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceProviderToL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceProviderToL2NetworkMsg.java @@ -4,6 +4,9 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.network.l2.L2NetworkVO; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachNetworkServiceProviderToL2NetworkMsg extends APIMessage { @APIParam diff --git a/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceToL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceToL3NetworkMsg.java index 158b7ad8680..c695b7ae5f8 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceToL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIAttachNetworkServiceToL3NetworkMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import java.util.*; +import org.zstack.header.vm.MetadataImpact; /** * @api attach network service to l3Network @@ -51,6 +52,7 @@ responseClass = APIAttachNetworkServiceToL3NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachNetworkServiceToL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceFromL3NetworkMsg.java b/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceFromL3NetworkMsg.java index 75143c3bcd7..febb3f7bb04 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceFromL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceFromL3NetworkMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import java.util.*; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 1/4/2016. @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachNetworkServiceFromL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachNetworkServiceFromL3NetworkMsg extends APIMessage implements L3NetworkMessage { /** * @desc l3Network uuid diff --git a/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceProviderFromL2NetworkMsg.java b/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceProviderFromL2NetworkMsg.java index 67704ca2908..c573da0b71d 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceProviderFromL2NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIDetachNetworkServiceProviderFromL2NetworkMsg.java @@ -4,6 +4,9 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.network.l2.L2NetworkVO; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachNetworkServiceProviderFromL2NetworkMsg extends APIMessage { @APIParam diff --git a/header/src/main/java/org/zstack/header/network/service/APIGetNetworkServiceTypesMsg.java b/header/src/main/java/org/zstack/header/network/service/APIGetNetworkServiceTypesMsg.java index 91e9aab1f44..a7be23aa6b4 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIGetNetworkServiceTypesMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIGetNetworkServiceTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get supported network service types @@ -33,6 +34,7 @@ method = HttpMethod.GET, responseClass = APIGetNetworkServiceTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetNetworkServiceTypesMsg extends APISyncCallMessage { public static APIGetNetworkServiceTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceL3NetworkRefMsg.java b/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceL3NetworkRefMsg.java index 5b4b64e71ba..daa82af5074 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceL3NetworkRefMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceL3NetworkRefMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryNetworkServiceL3NetworkRefReply.class, inventoryClass = NetworkServiceL3NetworkRefInventory.class) @RestRequest( @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryNetworkServiceL3NetworkRefReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryNetworkServiceL3NetworkRefMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceProviderMsg.java b/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceProviderMsg.java index f841d4619f8..86182e89e33 100755 --- a/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceProviderMsg.java +++ b/header/src/main/java/org/zstack/header/network/service/APIQueryNetworkServiceProviderMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryNetworkServiceProviderReply.class, inventoryClass = NetworkServiceProviderInventory.class) @RestRequest( @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryNetworkServiceProviderReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryNetworkServiceProviderMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/query/APIGenerateInventoryQueryDetailsMsg.java b/header/src/main/java/org/zstack/header/query/APIGenerateInventoryQueryDetailsMsg.java index eb530f0870c..0c74d3dadac 100755 --- a/header/src/main/java/org/zstack/header/query/APIGenerateInventoryQueryDetailsMsg.java +++ b/header/src/main/java/org/zstack/header/query/APIGenerateInventoryQueryDetailsMsg.java @@ -3,6 +3,9 @@ import org.zstack.header.message.APIMessage; import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateInventoryQueryDetailsMsg extends APIMessage { private String outputDir; diff --git a/header/src/main/java/org/zstack/header/query/APIGenerateQueryableFieldsMsg.java b/header/src/main/java/org/zstack/header/query/APIGenerateQueryableFieldsMsg.java index a9206dda29e..3b4622b6ee4 100755 --- a/header/src/main/java/org/zstack/header/query/APIGenerateQueryableFieldsMsg.java +++ b/header/src/main/java/org/zstack/header/query/APIGenerateQueryableFieldsMsg.java @@ -1,9 +1,11 @@ package org.zstack.header.query; import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateQueryableFieldsMsg extends APIMessage { public final static String PYTHON_FORMAT = "python"; private String format = PYTHON_FORMAT; diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeKeyMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeKeyMsg.java index b98d153f3be..fb1df47732c 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeKeyMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeKeyMsg.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(ResourceAttributeKeyVO.class) @RestRequest( @@ -20,6 +21,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.SECONDS, value = 5) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateResourceAttributeKeyMsg extends APICreateMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeValueMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeValueMsg.java index d9f1512164e..867d4a95a5c 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeValueMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APICreateResourceAttributeValueMsg.java @@ -12,6 +12,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/resource-attributes/{keyUuid}/resources", @@ -19,6 +20,7 @@ responseClass = APICreateResourceAttributeValueEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateResourceAttributeValueMsg extends APIMessage implements ResourceAttributeMessage { @APIParam(resourceType = ResourceAttributeKeyVO.class) private String keyUuid; diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeKeyMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeKeyMsg.java index 1f91b9e6917..49443f772e3 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeKeyMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeKeyMsg.java @@ -10,12 +10,14 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/resource-attributes/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteResourceAttributeKeyEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteResourceAttributeKeyMsg extends APIDeleteMessage implements ResourceAttributeMessage { @APIParam(resourceType = ResourceAttributeKeyVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeValueMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeValueMsg.java index 2d6b7641b56..0461d0d8101 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeValueMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APIDeleteResourceAttributeValueMsg.java @@ -11,12 +11,14 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/resource-attributes/{keyUuid}/resources", method = HttpMethod.DELETE, responseClass = APIDeleteResourceAttributeValueEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteResourceAttributeValueMsg extends APIMessage implements ResourceAttributeMessage { @APIParam(resourceType = ResourceAttributeKeyVO.class) private String keyUuid; diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeKeyMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeKeyMsg.java index 8a11c6c17a0..b7f97443724 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeKeyMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeKeyMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryResourceAttributeKeyReply.class, inventoryClass = ResourceAttributeKeyInventory.class) @RestRequest( @@ -17,6 +18,7 @@ responseClass = APIQueryResourceAttributeKeyReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryResourceAttributeKeyMsg extends APIQueryMessage { public static List __example__() { return list("name=OperationsPersonnel"); diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeValueMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeValueMsg.java index 13658675e41..14cd7a9add9 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeValueMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APIQueryResourceAttributeValueMsg.java @@ -10,6 +10,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryResourceAttributeValueReply.class, inventoryClass = ResourceAttributeValueInventory.class) @RestRequest( @@ -17,6 +18,7 @@ responseClass = APIQueryResourceAttributeValueReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryResourceAttributeValueMsg extends APIQueryMessage { public static List __example__() { return list("keyUuid=" + uuid(ResourceAttributeKeyVO.class)); diff --git a/header/src/main/java/org/zstack/header/resourceattribute/api/APIUpdateResourceAttributeKeyMsg.java b/header/src/main/java/org/zstack/header/resourceattribute/api/APIUpdateResourceAttributeKeyMsg.java index 65608ce50b2..9fd2d0ab124 100644 --- a/header/src/main/java/org/zstack/header/resourceattribute/api/APIUpdateResourceAttributeKeyMsg.java +++ b/header/src/main/java/org/zstack/header/resourceattribute/api/APIUpdateResourceAttributeKeyMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/resource-attributes/{uuid}/actions", @@ -16,6 +17,7 @@ isAction = true, responseClass = APIUpdateResourceAttributeKeyEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateResourceAttributeKeyMsg extends APIMessage implements ResourceAttributeMessage { @APIParam(required = true, resourceType = ResourceAttributeKeyVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/search/APICreateSearchIndexMsg.java b/header/src/main/java/org/zstack/header/search/APICreateSearchIndexMsg.java index d5d007adde1..327756b3939 100755 --- a/header/src/main/java/org/zstack/header/search/APICreateSearchIndexMsg.java +++ b/header/src/main/java/org/zstack/header/search/APICreateSearchIndexMsg.java @@ -4,6 +4,9 @@ import org.zstack.header.message.APIParam; import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateSearchIndexMsg extends APIMessage { @APIParam diff --git a/header/src/main/java/org/zstack/header/search/APIDeleteSearchIndexMsg.java b/header/src/main/java/org/zstack/header/search/APIDeleteSearchIndexMsg.java index 8dfda8036f9..742c93b070f 100755 --- a/header/src/main/java/org/zstack/header/search/APIDeleteSearchIndexMsg.java +++ b/header/src/main/java/org/zstack/header/search/APIDeleteSearchIndexMsg.java @@ -1,6 +1,9 @@ package org.zstack.header.search; import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteSearchIndexMsg extends APIMessage { private String indexName; diff --git a/header/src/main/java/org/zstack/header/storage/addon/backup/APIAddExternalBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/addon/backup/APIAddExternalBackupStorageMsg.java index 08f50f3c1d2..82b869ebca0 100755 --- a/header/src/main/java/org/zstack/header/storage/addon/backup/APIAddExternalBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/addon/backup/APIAddExternalBackupStorageMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.APIAddBackupStorageMsg; import org.zstack.header.storage.backup.BackupStorageConstant; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/backup-storage/addon", @@ -12,6 +13,7 @@ responseClass = APIAddExternalBackupStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddExternalBackupStorageMsg extends APIAddBackupStorageMsg { @APIParam(maxLength = 255, emptyString = false) private String identity; diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/APIAddExternalPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/addon/primary/APIAddExternalPrimaryStorageMsg.java index d791846f10d..f2a823da947 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/APIAddExternalPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/APIAddExternalPrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.storage.primary.APIAddPrimaryStorageEvent; import org.zstack.header.storage.primary.APIAddPrimaryStorageMsg; import org.zstack.header.storage.primary.PrimaryStorageConstant; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/addon", @@ -13,6 +14,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddExternalPrimaryStorageMsg extends APIAddPrimaryStorageMsg { @APIParam(maxLength = 255, emptyString = false) private String identity; diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/APIDiscoverExternalPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/addon/primary/APIDiscoverExternalPrimaryStorageMsg.java index 62a894a2fff..7c0dfcb5bf2 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/APIDiscoverExternalPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/APIDiscoverExternalPrimaryStorageMsg.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.MetadataImpact; @RestRequest( path = "/primary-storage/addon/discover", @@ -12,6 +13,7 @@ responseClass = APIDiscoverExternalPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDiscoverExternalPrimaryStorageMsg extends APIMessage { @APIParam @NoLogging(type = NoLogging.Type.Uri) diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/APIUpdateExternalPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/addon/primary/APIUpdateExternalPrimaryStorageMsg.java index eba3533747d..01a527ba213 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/APIUpdateExternalPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/APIUpdateExternalPrimaryStorageMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.APIUpdatePrimaryStorageMsg; import org.zstack.header.storage.primary.PrimaryStorageMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/addon/{uuid}/actions", @@ -12,6 +13,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateExternalPrimaryStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateExternalPrimaryStorageMsg extends APIUpdatePrimaryStorageMsg implements PrimaryStorageMessage { @APIParam(required = false) private String config; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIAddBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIAddBackupStorageMsg.java index c52d0686d0b..7aab12590d9 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIAddBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIAddBackupStorageMsg.java @@ -6,6 +6,9 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public abstract class APIAddBackupStorageMsg extends APICreateMessage implements APIAuditor { /** diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIAttachBackupStorageToZoneMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIAttachBackupStorageToZoneMsg.java index 029c1da5706..366217e6337 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIAttachBackupStorageToZoneMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIAttachBackupStorageToZoneMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; /** * @api attach backup storage to a zone @@ -40,6 +41,7 @@ parameterName = "params", responseClass = APIAttachBackupStorageToZoneEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachBackupStorageToZoneMsg extends APIMessage implements BackupStorageMessage { /** * @desc zone uuid. See :ref:`ZoneInventory` diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIChangeBackupStorageStateMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIChangeBackupStorageStateMsg.java index 3c270ab6197..f5413a73baf 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIChangeBackupStorageStateMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIChangeBackupStorageStateMsg.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.MetadataImpact; /** * @api change backup storage state @@ -39,6 +40,7 @@ responseClass = APIChangeBackupStorageStateEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeBackupStorageStateMsg extends APIMessage implements BackupStorageMessage { /** * @desc backup storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/backup/APICleanUpTrashOnBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APICleanUpTrashOnBackupStorageMsg.java index 169dccbbd48..93a8dfb5146 100644 --- a/header/src/main/java/org/zstack/header/storage/backup/APICleanUpTrashOnBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APICleanUpTrashOnBackupStorageMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/10. @@ -17,6 +18,7 @@ responseClass = APICleanUpTrashOnBackupStorageEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanUpTrashOnBackupStorageMsg extends APIMessage implements BackupStorageMessage, APIBatchRequest { @APIParam(resourceType = BackupStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIDeleteBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIDeleteBackupStorageMsg.java index 7af25d380e8..1ef14ba0f90 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIDeleteBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIDeleteBackupStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api delete backup storage @@ -39,6 +40,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteBackupStorageMsg extends APIDeleteMessage implements BackupStorageMessage { /** * @desc backup storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIDeleteExportedImageFromBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIDeleteExportedImageFromBackupStorageMsg.java index dd1a15d0b1f..6ecdb840d9d 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIDeleteExportedImageFromBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIDeleteExportedImageFromBackupStorageMsg.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.MetadataImpact; /** * Created by david on 8/31/16. @@ -14,6 +15,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteExportedImageFromBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteExportedImageFromBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = BackupStorageVO.class) private String backupStorageUuid; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIDetachBackupStorageFromZoneMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIDetachBackupStorageFromZoneMsg.java index 94f3fd1b139..767d2bb2019 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIDetachBackupStorageFromZoneMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIDetachBackupStorageFromZoneMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; /** * @api detach backup storage from a zone @@ -39,6 +40,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachBackupStorageFromZoneEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachBackupStorageFromZoneMsg extends APIMessage implements BackupStorageMessage { /** * @desc backup storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageCapacityMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageCapacityMsg.java index 9321f487b9c..043e5023dd5 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageCapacityMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageCapacityMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIGetBackupStorageCapacityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetBackupStorageCapacityMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = ZoneVO.class) private List zoneUuids; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageTypesMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageTypesMsg.java index 3a2c9a6bd4a..d326fec9b4d 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageTypesMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIGetBackupStorageTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get supported backup storage types @@ -32,6 +33,7 @@ method = HttpMethod.GET, responseClass = APIGetBackupStorageTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetBackupStorageTypesMsg extends APISyncCallMessage { public static APIGetBackupStorageTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIGetTrashOnBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIGetTrashOnBackupStorageMsg.java index ace05dd66d8..ccbac41d834 100644 --- a/header/src/main/java/org/zstack/header/storage/backup/APIGetTrashOnBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIGetTrashOnBackupStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/10. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetTrashOnBackupStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetTrashOnBackupStorageMsg extends APISyncCallMessage implements BackupStorageMessage { @APIParam(resourceType = BackupStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIQueryBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIQueryBackupStorageMsg.java index 781924361be..660ae026412 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIQueryBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIQueryBackupStorageMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryBackupStorageReply.class, inventoryClass = BackupStorageInventory.class) @RestRequest( @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryBackupStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryBackupStorageMsg extends APIQueryMessage { diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIReconnectBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIReconnectBackupStorageMsg.java index 9f1d92cab24..ec00884e4f0 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIReconnectBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIReconnectBackupStorageMsg.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.MetadataImpact; /** * Created by xing5 on 2016/4/9. @@ -15,6 +16,7 @@ responseClass = APIReconnectBackupStorageEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = BackupStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIScanBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIScanBackupStorageMsg.java index 7f2a47729a5..0709a7bd0c3 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIScanBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIScanBackupStorageMsg.java @@ -1,6 +1,9 @@ package org.zstack.header.storage.backup; import org.zstack.header.message.APIMessage; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIScanBackupStorageMsg extends APIMessage implements BackupStorageMessage { private String backupStorageUuid; diff --git a/header/src/main/java/org/zstack/header/storage/backup/APIUpdateBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/backup/APIUpdateBackupStorageMsg.java index 40d6e490187..81ee6a1d346 100755 --- a/header/src/main/java/org/zstack/header/storage/backup/APIUpdateBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/backup/APIUpdateBackupStorageMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = BackupStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIAddPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIAddPrimaryStorageMsg.java index 75523f1f54f..871a762099a 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIAddPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIAddPrimaryStorageMsg.java @@ -7,6 +7,9 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; + +@MetadataImpact(MetadataImpact.Impact.NONE) public abstract class APIAddPrimaryStorageMsg extends APICreateMessage implements APIAuditor { /** diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIAddStorageProtocolMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIAddStorageProtocolMsg.java index 8a8dba7203f..1c704e96007 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIAddStorageProtocolMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIAddStorageProtocolMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.volume.VolumeProtocol; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/protocols", @@ -12,6 +13,7 @@ method = HttpMethod.POST, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddStorageProtocolMsg extends APIMessage implements PrimaryStorageMessage { /** * @desc primary storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIAttachPrimaryStorageToClusterMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIAttachPrimaryStorageToClusterMsg.java index 05a32a3e5cc..2cb209770e8 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIAttachPrimaryStorageToClusterMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIAttachPrimaryStorageToClusterMsg.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.MetadataImpact; /** * @api attach primary storage to a cluster @@ -39,6 +40,7 @@ parameterName = "params", responseClass = APIAttachPrimaryStorageToClusterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachPrimaryStorageToClusterMsg extends APIMessage implements PrimaryStorageMessage { /** * @desc uuid of cluster this primary storage is attaching to diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIChangePrimaryStorageStateMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIChangePrimaryStorageStateMsg.java index 6648e8d6603..c7e3d492260 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIChangePrimaryStorageStateMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIChangePrimaryStorageStateMsg.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.MetadataImpact; /** * @api change primary storage state @@ -38,6 +39,7 @@ responseClass = APIChangePrimaryStorageStateEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangePrimaryStorageStateMsg extends APIMessage implements PrimaryStorageMessage { /** * @desc primary storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpImageCacheOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpImageCacheOnPrimaryStorageMsg.java index 0604aa016c3..1946126614a 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpImageCacheOnPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpImageCacheOnPrimaryStorageMsg.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.MetadataImpact; /** * Created by xing5 on 2016/7/21. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APICleanUpImageCacheOnPrimaryStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanUpImageCacheOnPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpStorageTrashOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpStorageTrashOnPrimaryStorageMsg.java index 74bc74f952b..1bb55aa3b42 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpStorageTrashOnPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpStorageTrashOnPrimaryStorageMsg.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.MetadataImpact; @RestRequest( path = "/primary-storage/{uuid}/storagetrash/actions", @@ -11,6 +12,7 @@ responseClass = APICleanUpStorageTrashOnPrimaryStorageEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanUpStorageTrashOnPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpTrashOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpTrashOnPrimaryStorageMsg.java index 0c7e66b2249..728494f1569 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APICleanUpTrashOnPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APICleanUpTrashOnPrimaryStorageMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/10. @@ -17,6 +18,7 @@ responseClass = APICleanUpTrashOnPrimaryStorageEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanUpTrashOnPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage, APIBatchRequest { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIDeletePrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIDeletePrimaryStorageMsg.java index 1598c438001..0a0e5d768c3 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIDeletePrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIDeletePrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api delete a primary storage @@ -39,6 +40,7 @@ method = HttpMethod.DELETE, responseClass = APIDeletePrimaryStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeletePrimaryStorageMsg extends APIDeleteMessage implements PrimaryStorageMessage { /** * @desc primary storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIDetachPrimaryStorageFromClusterMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIDetachPrimaryStorageFromClusterMsg.java index c37616d1101..48cd33c7848 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIDetachPrimaryStorageFromClusterMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIDetachPrimaryStorageFromClusterMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api detach primary storage from a cluster @@ -39,6 +40,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachPrimaryStorageFromClusterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachPrimaryStorageFromClusterMsg extends APIMessage implements PrimaryStorageMessage { /** * @desc primary storage uuid diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageAllocatorStrategiesMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageAllocatorStrategiesMsg.java index b87cf8518f7..817221f1e98 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageAllocatorStrategiesMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageAllocatorStrategiesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get allocation strategy of primary storage @@ -32,6 +33,7 @@ method = HttpMethod.GET, responseClass = APIGetPrimaryStorageAllocatorStrategiesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPrimaryStorageAllocatorStrategiesMsg extends APISyncCallMessage { public static APIGetPrimaryStorageAllocatorStrategiesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageCapacityMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageCapacityMsg.java index 2511ce14287..ff6557d2f01 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageCapacityMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageCapacityMsg.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIGetPrimaryStorageCapacityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPrimaryStorageCapacityMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = ZoneVO.class) private List zoneUuids; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageLicenseInfoMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageLicenseInfoMsg.java index e6745d4e861..b0465443ae7 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageLicenseInfoMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageLicenseInfoMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/{uuid}/license", method = HttpMethod.GET, responseClass = APIGetPrimaryStorageLicenseInfoReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPrimaryStorageLicenseInfoMsg extends APISyncCallMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageTypesMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageTypesMsg.java index a8a617792a2..177dd322069 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageTypesMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageTypesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api get supported primary storage type @@ -32,6 +33,7 @@ method = HttpMethod.GET, responseClass = APIGetPrimaryStorageTypesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPrimaryStorageTypesMsg extends APISyncCallMessage { public static APIGetPrimaryStorageTypesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageUsageReportMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageUsageReportMsg.java index e330ad9f53c..84d0871b83c 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageUsageReportMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetPrimaryStorageUsageReportMsg.java @@ -6,12 +6,14 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/{primaryStorageUuid}/usage/report", method = HttpMethod.GET, responseClass = APIGetPrimaryStorageUsageReportReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPrimaryStorageUsageReportMsg extends APISyncCallMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String primaryStorageUuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetTrashOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetTrashOnPrimaryStorageMsg.java index 5b8d58d3fd3..8d446625630 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetTrashOnPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetTrashOnPrimaryStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/12/10. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetTrashOnPrimaryStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetTrashOnPrimaryStorageMsg extends APISyncCallMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java index aab3976a182..8de8b17186f 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( @@ -11,6 +12,7 @@ method = HttpMethod.GET, responseClass = APIGetVmInstanceMetadataFromPrimaryStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmInstanceMetadataFromPrimaryStorageMsg extends APISyncCallMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIQueryImageCacheMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIQueryImageCacheMsg.java index 98645d22fd9..23296d07546 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIQueryImageCacheMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIQueryImageCacheMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by mingjian.deng on 2018/5/25. @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryImageCacheReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryImageCacheMsg extends APIQueryMessage { public static List __example__() { return Collections.singletonList("uuid=" + uuid()); diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIQueryPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIQueryPrimaryStorageMsg.java index 37a3aa16c55..137696190ec 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIQueryPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIQueryPrimaryStorageMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryPrimaryStorageReply.class, inventoryClass = PrimaryStorageInventory.class) @RestRequest( @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIQueryPrimaryStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryPrimaryStorageMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIReconnectPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIReconnectPrimaryStorageMsg.java index 91ee7f35407..d037a44b015 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIReconnectPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIReconnectPrimaryStorageMsg.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.MetadataImpact; /** * Created by frank on 4/23/2015. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java index 4b2c4e80778..b2ce14b9116 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIRegisterVmInstanceMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/register", @@ -13,6 +14,7 @@ responseClass = APIRegisterVmInstanceReply.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRegisterVmInstanceMsg extends APIMessage implements PrimaryStorageMessage { @APIParam() private String metadataPath; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APISyncPrimaryStorageCapacityMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APISyncPrimaryStorageCapacityMsg.java index 8836ad428fd..0ad735a1d7c 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APISyncPrimaryStorageCapacityMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APISyncPrimaryStorageCapacityMsg.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.MetadataImpact; /** * Created by frank on 6/18/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISyncPrimaryStorageCapacityMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String primaryStorageUuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIUpdatePrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIUpdatePrimaryStorageMsg.java index 76cc2027965..4d79d1bd416 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/APIUpdatePrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIUpdatePrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -16,6 +17,7 @@ method = HttpMethod.PUT, responseClass = APIUpdatePrimaryStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdatePrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIBackupVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIBackupVolumeSnapshotMsg.java index 7184f41af67..4ddea79b710 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIBackupVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIBackupVolumeSnapshotMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.APINoSee; import org.zstack.header.storage.backup.BackupStorageVO; +import org.zstack.header.vm.MetadataImpact; /** * @api back up volume snapshot to backup storage @@ -32,6 +33,8 @@ * @since 0.1.0 */ +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APIBackupVolumeSnapshotMsg extends APIMessage implements VolumeSnapshotMessage { /** * @desc volume snapshot uuid diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIBatchDeleteVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIBatchDeleteVolumeSnapshotMsg.java index 5f1aa849941..4ded804a054 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIBatchDeleteVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIBatchDeleteVolumeSnapshotMsg.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/volume-snapshots/batch-delete", @@ -25,6 +26,7 @@ responseClass = APIBatchDeleteVolumeSnapshotEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 6) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIBatchDeleteVolumeSnapshotMsg extends APIDeleteMessage implements APIMultiAuditor, APIBatchRequest { /** * @desc volume snapshot uuid diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotFromBackupStorageMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotFromBackupStorageMsg.java index 47403414bc9..407335a3d82 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotFromBackupStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotFromBackupStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.storage.backup.BackupStorageVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @api delete a copy of volume snapshot from one or more backup storage @@ -41,6 +42,7 @@ * @result see :ref:`APIDeleteVolumeSnapshotFromBackupStorageEvent` * @since 0.1.0 */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVolumeSnapshotFromBackupStorageMsg extends APIDeleteMessage implements VolumeSnapshotMessage { /** * @desc volume snapshot uuid diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIGetVolumeSnapshotSizeMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIGetVolumeSnapshotSizeMsg.java index 0403a2e925f..81360a2b254 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIGetVolumeSnapshotSizeMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIGetVolumeSnapshotSizeMsg.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.MetadataImpact; /** * Created by lining on 2019/5/14. @@ -15,6 +16,7 @@ responseClass = APIGetVolumeSnapshotSizeEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVolumeSnapshotSizeMsg extends APIMessage implements VolumeSnapshotMessage { @APIParam(resourceType = VolumeSnapshotVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotMsg.java index bfd2d0d6edb..b0dfd4d43fa 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryVolumeSnapshotReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVolumeSnapshotMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeMsg.java index fb4ead85946..5d0977327cd 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -17,6 +18,7 @@ responseClass = APIQueryVolumeSnapshotTreeReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVolumeSnapshotTreeMsg extends APIQueryMessage { public static List __example__() { 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..ad19a7257ad 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 @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @ Author : yh.w @@ -20,6 +21,7 @@ isAction = true ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..5a08b7863f6 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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVolumeSnapshotEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APICheckMemorySnapshotGroupConflictMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckMemorySnapshotGroupConflictMsg.java index 5dbc959426c..6b0839e0e55 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckMemorySnapshotGroupConflictMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckMemorySnapshotGroupConflictMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/memory-snapshots/groups/{uuid}/conflict-detection", method = HttpMethod.GET, responseClass = APICheckMemorySnapshotGroupConflictReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckMemorySnapshotGroupConflictMsg extends APISyncCallMessage { @APIParam(resourceType = VolumeSnapshotGroupVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckVolumeSnapshotGroupAvailabilityMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckVolumeSnapshotGroupAvailabilityMsg.java index cee06e2356f..abd4de2bde1 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckVolumeSnapshotGroupAvailabilityMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APICheckVolumeSnapshotGroupAvailabilityMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2019/7/12. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APICheckVolumeSnapshotGroupAvailabilityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckVolumeSnapshotGroupAvailabilityMsg extends APISyncCallMessage { @APIParam(resourceType = VolumeSnapshotGroupVO.class) private List uuids; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIQueryVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIQueryVolumeSnapshotGroupMsg.java index 5c2a7ade56e..cdeccab709a 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIQueryVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIQueryVolumeSnapshotGroupMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2019/7/11. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryVolumeSnapshotGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVolumeSnapshotGroupMsg extends APIQueryMessage { public static List __example__() { return Collections.singletonList("uuid=" + 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..882e4dacfab 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 @@ -16,6 +16,7 @@ import org.zstack.header.vm.VmInstanceVO; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/volume-snapshots/group/{uuid}/actions", @@ -25,6 +26,7 @@ ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..5fb32d32378 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.MetadataImpact; /** * Created by MaJin on 2019/7/9. @@ -14,6 +15,7 @@ method = HttpMethod.DELETE, responseClass = APIUngroupVolumeSnapshotGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..5deef3c7aa0 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 @@ -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.MetadataImpact; /** * Created by MaJin on 2019/8/29. @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVolumeSnapshotGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVolumeSnapshotGroupMsg extends APIMessage implements VolumeSnapshotGroupMessage { @APIParam(required = false) private String name; diff --git a/header/src/main/java/org/zstack/header/tag/APIAbstractCreateTagMsg.java b/header/src/main/java/org/zstack/header/tag/APIAbstractCreateTagMsg.java index b0d3efd7f2a..9a157947f35 100755 --- a/header/src/main/java/org/zstack/header/tag/APIAbstractCreateTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APIAbstractCreateTagMsg.java @@ -4,9 +4,11 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; /** */ +@MetadataImpact(MetadataImpact.Impact.NONE) public abstract class APIAbstractCreateTagMsg extends APIMessage { @APIParam private String resourceType; 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..4024171a006 100644 --- a/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/system-tags/{resourceUuid}/tags", @@ -16,6 +17,7 @@ responseClass = APICreateSystemTagsEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateSystemTagsMsg extends APIMessage { @APIParam private String resourceType; diff --git a/header/src/main/java/org/zstack/header/tag/APIQuerySystemTagMsg.java b/header/src/main/java/org/zstack/header/tag/APIQuerySystemTagMsg.java index 1d436d86218..913b23b6a48 100755 --- a/header/src/main/java/org/zstack/header/tag/APIQuerySystemTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APIQuerySystemTagMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQuerySystemTagReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySystemTagMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/tag/APIQueryUserTagMsg.java b/header/src/main/java/org/zstack/header/tag/APIQueryUserTagMsg.java index 854807b2077..91eb66f0ccd 100755 --- a/header/src/main/java/org/zstack/header/tag/APIQueryUserTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APIQueryUserTagMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ responseClass = APIQueryUserTagReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryUserTagMsg extends APIQueryMessage { public static List __example__() { 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..9ba7af5b17e 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.MetadataImpact; /** * Created by frank on 10/17/2015. @@ -16,6 +17,7 @@ responseClass = APIAttachIsoToVmInstanceEvent.class, parameterName = "null" ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..58215ce31fc 100755 --- a/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; /** * @api attach a nic to vm. If vm is running, user is responsible for running DHCP client software inside @@ -45,6 +46,7 @@ method = HttpMethod.POST, responseClass = APIAttachL3NetworkToVmEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachL3NetworkToVmMsg extends APIMessage implements VmInstanceMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmNicMsg.java b/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmNicMsg.java index 0be2381e76f..1ee46d44fe5 100755 --- a/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmNicMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmNicMsg.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.MetadataImpact; @RestRequest( path = "/nics/{vmNicUuid}/l3-networks/{l3NetworkUuid}", @@ -13,6 +14,7 @@ responseClass = APIAttachL3NetworkToVmNicEvent.class ) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachL3NetworkToVmNicMsg extends APIMessage { /** * @desc vm uuid 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..373fea09524 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.MetadataImpact; /** * Created by frank on 7/16/2015. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APIChangeInstanceOfferingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APICheckVmInstanceMetadataConsistencyMsg.java b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyMsg.java index df4da4eee67..f1904a7af53 100644 --- a/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICheckVmInstanceMetadataConsistencyMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.storage.primary.PrimaryStorageVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/metadata/consistency-check", @@ -14,6 +15,7 @@ responseClass = APICheckVmInstanceMetadataConsistencyReply.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckVmInstanceMetadataConsistencyMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = VmInstanceVO.class) private List vmUuids; diff --git a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java index a8003a15428..8839790045d 100644 --- a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.storage.primary.PrimaryStorageVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/metadata/cleanup", @@ -14,6 +15,7 @@ responseClass = APICleanupVmInstanceMetadataEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICleanupVmInstanceMetadataMsg extends APIMessage { @APIParam(required = false, resourceType = PrimaryStorageVO.class) private List primaryStorageUuids; diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeMsg.java b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeMsg.java index ffb586384f5..d8365ba74b1 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeMsg.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2020/6/29. @@ -29,6 +30,7 @@ ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVmInstanceFromVolumeMsg extends APICreateMessage implements APIAuditor, NewVmInstanceMessage2 { @APIParam(maxLength = 255) private String name; 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..8f167e67e10 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2021/3/10. @@ -32,6 +33,8 @@ parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APICreateVmInstanceFromVolumeSnapshotGroupMsg extends APICreateMessage implements NewVmInstanceMessage2, APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotMsg.java index 8302f6046fc..41da923b4bb 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotMsg.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2021/3/10. @@ -28,6 +29,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVmInstanceFromVolumeSnapshotMsg extends APICreateMessage implements NewVmInstanceMessage2, APIAuditor { @APIParam(maxLength = 255) private String name; 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..94317950bc4 100755 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api create a new vm instance @@ -79,6 +80,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 12) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVmInstanceMsg extends APICreateMessage implements APIAuditor, NewVmInstanceMessage2 { /** * @desc max length of 255 characters diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmNicMsg.java b/header/src/main/java/org/zstack/header/vm/APICreateVmNicMsg.java index 4e9cd9c8545..5079783587d 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmNicMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmNicMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(VmNicVO.class) @@ -18,6 +19,7 @@ responseClass = APICreateVmNicEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVmNicMsg extends APICreateMessage implements APIAuditor { /** diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteTemplatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteTemplatedVmInstanceMsg.java index 5b7af935f4d..443a7699214 100644 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteTemplatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteTemplatedVmInstanceMsg.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.MetadataImpact; @RestRequest( path = "/vm-instances/templatedVmInstance/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteTemplatedVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteTemplatedVmInstanceMsg extends APIDeleteMessage implements VmInstanceMessage { @APIParam(resourceType = TemplatedVmInstanceVO.class, successIfResourceNotExisting = true) 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..6a761676eef 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.MetadataImpact; /** * Created by root on 8/2/16. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmConsolePasswordEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APIDeleteVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java index 15d11f96647..70d6762b576 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.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmStaticIpEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..0f7bf777e2b 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api destroy a vm instance @@ -41,6 +42,7 @@ method = HttpMethod.DELETE, responseClass = APIDestroyVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..ee0cf2ebca0 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.MetadataImpact; /** * Created by frank on 10/17/2015. @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachIsoFromVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..de104da6322 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/18/2015. @@ -21,6 +22,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachL3NetworkFromVmEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APIExpungeVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIExpungeVmInstanceMsg.java index 7b9ab119a4f..cfa9d7c560e 100755 --- a/header/src/main/java/org/zstack/header/vm/APIExpungeVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIExpungeVmInstanceMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.RestRequest; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/12/2015. @@ -18,6 +19,7 @@ method = HttpMethod.PUT ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIExpungeVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIFlattenVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIFlattenVmInstanceMsg.java index b7108cbe883..d19600448e7 100644 --- a/header/src/main/java/org/zstack/header/vm/APIFlattenVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIFlattenVmInstanceMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.rest.RestRequest; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 36) @RestRequest(path = "/vm-instances/{uuid}/actions", @@ -17,6 +18,7 @@ responseClass = APIFlattenVmInstanceEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIFlattenVmInstanceMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIFstrimVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIFstrimVmMsg.java index e09e3c7232d..d60bc0ae753 100644 --- a/header/src/main/java/org/zstack/header/vm/APIFstrimVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIFstrimVmMsg.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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -12,6 +13,7 @@ method = HttpMethod.POST, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIFstrimVmMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidateIsoForAttachingVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidateIsoForAttachingVmMsg.java index 5f03ae0a3c6..50e7a9710c8 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidateIsoForAttachingVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidateIsoForAttachingVmMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/9/21. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateIsoForAttachingVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateIsoForAttachingVmMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidateL3NetworksForChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidateL3NetworksForChangeVmNicNetworkMsg.java index 304b7a841a5..c3965ec99f8 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidateL3NetworksForChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidateL3NetworksForChangeVmNicNetworkMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/nics/{vmNicUuid}/l3-networks-candidates", method = HttpMethod.GET, responseClass = APIGetCandidateL3NetworksForChangeVmNicNetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateL3NetworksForChangeVmNicNetworkMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidatePrimaryStoragesForCreatingVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidatePrimaryStoragesForCreatingVmMsg.java index 2e76ed13baa..fe4c28d5c7d 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidatePrimaryStoragesForCreatingVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidatePrimaryStoragesForCreatingVmMsg.java @@ -11,6 +11,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2017-08-16. @@ -21,6 +22,8 @@ responseClass = APIGetCandidatePrimaryStoragesForCreatingVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APIGetCandidatePrimaryStoragesForCreatingVmMsg extends APISyncCallMessage { @APIParam(resourceType = ImageVO.class) private String imageUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidateVmForAttachingIsoMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidateVmForAttachingIsoMsg.java index fe0147ee331..f6306110fa6 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidateVmForAttachingIsoMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidateVmForAttachingIsoMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/9/20. @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateVmForAttachingIsoReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateVmForAttachingIsoMsg extends APISyncCallMessage { @APIParam(resourceType = ImageVO.class) private String isoUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java index 264898399f5..561a1cbbe89 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java @@ -12,6 +12,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/8/17. @@ -21,6 +22,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateZonesClustersHostsForCreatingVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateZonesClustersHostsForCreatingVmMsg extends APISyncCallMessage { @APIParam(resourceType = InstanceOfferingVO.class, required = false) private String instanceOfferingUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksBackupStoragesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksBackupStoragesMsg.java index bfb904105d9..3ccb4f84701 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksBackupStoragesMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksBackupStoragesMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.zone.ZoneVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by Qi Le on 2022/3/9 @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIGetInterdependentL3NetworksBackupStoragesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetInterdependentL3NetworksBackupStoragesMsg extends APISyncCallMessage { @APIParam(resourceType = ZoneVO.class) private String zoneUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksImagesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksImagesMsg.java index cdd3a7b0d2b..1ffabcf949d 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksImagesMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetInterdependentL3NetworksImagesMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.zone.ZoneVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/8/23. @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIGetInterdependentL3NetworkImageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetInterdependentL3NetworksImagesMsg extends APISyncCallMessage { @APIParam(resourceType = ZoneVO.class) private String zoneUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetMemorySnapshotGroupReferenceMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetMemorySnapshotGroupReferenceMsg.java index b1fddc52526..8e6f049072c 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetMemorySnapshotGroupReferenceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetMemorySnapshotGroupReferenceMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by LiangHanYu on 2022/7/5 17:44 @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIGetMemorySnapshotGroupReferenceReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetMemorySnapshotGroupReferenceMsg extends APISyncCallMessage { @APIParam(resourceType = ResourceVO.class) private String resourceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetSpiceCertificatesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetSpiceCertificatesMsg.java index f3b1c757da2..57da22d46a4 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetSpiceCertificatesMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetSpiceCertificatesMsg.java @@ -3,12 +3,14 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/spice/certificates", method = HttpMethod.GET, responseClass = APIGetSpiceCertificatesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetSpiceCertificatesMsg extends APISyncCallMessage { public static APIGetSpiceCertificatesMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableDataVolumeMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableDataVolumeMsg.java index 837ed548812..03544d16418 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableDataVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetVmAttachableDataVolumeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmAttachableDataVolumeMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableL3NetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableL3NetworkMsg.java index 2a49d7b1f26..920c3e815c7 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableL3NetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmAttachableL3NetworkMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/19/2015. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmAttachableL3NetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmAttachableL3NetworkMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmBootOrderMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmBootOrderMsg.java index ae80b8b5c64..49aa9cfae9b 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmBootOrderMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmBootOrderMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/22/2015. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmBootOrderReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmBootOrderMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmCapabilitiesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmCapabilitiesMsg.java index fa90b3896ae..2ed48799e56 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmCapabilitiesMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmCapabilitiesMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/5/17. @@ -13,6 +14,7 @@ responseClass = APIGetVmCapabilitiesReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmCapabilitiesMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmConsoleAddressMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmConsoleAddressMsg.java index fdd617957ff..aa897debd5e 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmConsoleAddressMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmConsoleAddressMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 1/25/2016. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmConsoleAddressReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmConsoleAddressMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmConsolePasswordMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmConsolePasswordMsg.java index c206601c196..5f4dec8bd57 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmConsolePasswordMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmConsolePasswordMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by root on 7/29/16. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmConsolePasswordReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmConsolePasswordMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmDeviceAddressMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmDeviceAddressMsg.java index c2c8cce88f1..6cf463902e0 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmDeviceAddressMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmDeviceAddressMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2020/7/22. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIGetVmDeviceAddressReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmDeviceAddressMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmDnsMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmDnsMsg.java index 299979ea021..9c008bde14f 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmDnsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmDnsMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.utils.network.IPv6Constants; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/{vmInstanceUuid}/dns", method = HttpMethod.GET, responseClass = APIGetVmDnsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmDnsMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmHostnameMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmHostnameMsg.java index 514495932c1..5c974ee4af5 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmHostnameMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmHostnameMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/4/8. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmHostnameReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmHostnameMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java index 1a0a82455e7..2b1963ae447 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetVmMigrationCandidateHostsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmMigrationCandidateHostsMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmNicAttachedNetworkServiceMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmNicAttachedNetworkServiceMsg.java index b467e3ca2cf..630a1371d04 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmNicAttachedNetworkServiceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmNicAttachedNetworkServiceMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/nics/{vmNicUuid}/attached-networkservices", method = HttpMethod.GET, responseClass = APIGetVmNicAttachedNetworkServiceReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmNicAttachedNetworkServiceMsg extends APISyncCallMessage { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmSshKeyMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmSshKeyMsg.java index 3aa95c1907d..b2344adc1e3 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmSshKeyMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmSshKeyMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by luchukun on 8/4/16. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmSshKeyReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmSshKeyMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java index 36da0205032..4f0b5f19ccf 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/5/14. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmStartingCandidateClustersHostsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmStartingCandidateClustersHostsMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmTaskMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmTaskMsg.java index 8af717225af..5ba3539735e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmTaskMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmTaskMsg.java @@ -11,12 +11,14 @@ import java.util.function.Function; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/task-details", method = HttpMethod.GET, responseClass = APIGetChainTaskReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmTaskMsg extends APIGetChainTaskMsg { @APIParam(nonempty = true, resourceType = VmInstanceVO.class) private List vmInstanceUuids; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmUptimeMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmUptimeMsg.java index c264e9fcb5b..7aa7861e376 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmUptimeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmUptimeMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/uptime", method = HttpMethod.GET, responseClass = APIGetVmUptimeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmUptimeMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmsCapabilitiesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmsCapabilitiesMsg.java index b8266c66e8e..849c29e8aec 100644 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmsCapabilitiesMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmsCapabilitiesMsg.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @ Author : yh.w @@ -18,6 +19,7 @@ responseClass = APIGetVmsCapabilitiesEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmsCapabilitiesMsg extends APIMessage { @APIParam(nonempty = true) private List vmUuids; 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..934e5840f9a 100755 --- a/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @api live migrate vm to another host @@ -44,6 +45,7 @@ ) @SkipVmTracer(replyClass = APIMigrateVmEvent.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIMigrateVmMsg extends APIMessage implements VmInstanceMessage, MigrateVmMessage, CheckAttachedVolumesMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIPauseVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIPauseVmInstanceMsg.java index 5d8edfee577..e180691c3c2 100755 --- a/header/src/main/java/org/zstack/header/vm/APIPauseVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIPauseVmInstanceMsg.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.MetadataImpact; /** * Created by root on 10/29/16. @@ -15,6 +16,7 @@ responseClass = APIPauseVmInstanceEvent.class ) @SkipVmTracer(replyClass = APIPauseVmInstanceEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIPauseVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java index 68912504843..884aa746976 100644 --- a/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIPreCheckVmMetadataRegistrationMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.cluster.ClusterVO; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/metadata/pre-check", @@ -14,6 +15,7 @@ responseClass = APIPreCheckVmMetadataRegistrationReply.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIPreCheckVmMetadataRegistrationMsg extends APISyncCallMessage { @APIParam private String metadataContent; diff --git a/header/src/main/java/org/zstack/header/vm/APIQueryTemplatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIQueryTemplatedVmInstanceMsg.java index 79ffe9aa861..dae7c0fc958 100644 --- a/header/src/main/java/org/zstack/header/vm/APIQueryTemplatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIQueryTemplatedVmInstanceMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryTemplatedVmInstanceReply.class, inventoryClass = TemplatedVmInstanceInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryTemplatedVmInstanceReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryTemplatedVmInstanceMsg extends APIQueryMessage { public static List __example__() { return asList("uuid=" + uuid()); diff --git a/header/src/main/java/org/zstack/header/vm/APIQueryVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIQueryVmInstanceMsg.java index dc40b281025..71b15236777 100755 --- a/header/src/main/java/org/zstack/header/vm/APIQueryVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIQueryVmInstanceMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVmInstanceReply.class, inventoryClass = VmInstanceInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryVmInstanceReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmInstanceMsg extends APIQueryMessage { public static List __example__() { return asList("name=vm1", "vmNics.ip=192.168.20.100"); diff --git a/header/src/main/java/org/zstack/header/vm/APIQueryVmNicMsg.java b/header/src/main/java/org/zstack/header/vm/APIQueryVmNicMsg.java index 0723ac16931..0e3330cd0ea 100755 --- a/header/src/main/java/org/zstack/header/vm/APIQueryVmNicMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIQueryVmNicMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVmNicReply.class, inventoryClass = VmNicInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryVmNicReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmNicMsg extends APIQueryMessage { public static List __example__() { return asList("ip=172.20.100.100"); diff --git a/header/src/main/java/org/zstack/header/vm/APIQueryVmPriorityConfigMsg.java b/header/src/main/java/org/zstack/header/vm/APIQueryVmPriorityConfigMsg.java index 993020af315..c78b3870c2f 100644 --- a/header/src/main/java/org/zstack/header/vm/APIQueryVmPriorityConfigMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIQueryVmPriorityConfigMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVmPriorityConfigReply.class, inventoryClass = VmPriorityConfigInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryVmPriorityConfigReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmPriorityConfigMsg extends APIQueryMessage { public static List __example__() { return asList("uuid=" + uuid()); diff --git a/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java index db0ba2966cd..756ad7f8dd0 100644 --- a/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIReadVmInstanceMetadataMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/{vmUuid}/metadata", method = HttpMethod.GET, responseClass = APIReadVmInstanceMetadataReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReadVmInstanceMetadataMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIRebootVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIRebootVmInstanceMsg.java index 410ec6c04af..cea0d204eab 100755 --- a/header/src/main/java/org/zstack/header/vm/APIRebootVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIRebootVmInstanceMsg.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.MetadataImpact; /** * @api reboot a vm instance @@ -37,6 +38,7 @@ responseClass = APIRebootVmInstanceEvent.class ) @SkipVmTracer(replyClass = APIRebootVmInstanceEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRebootVmInstanceMsg extends APIMessage implements VmInstanceMessage, CheckAttachedVolumesMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java index 11e3a8a0db5..638a4d412b9 100644 --- a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.tag.TagResourceType; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(VmInstanceVO.class) @RestRequest( @@ -20,6 +21,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRegisterVmInstanceFromMetadataMsg extends APICreateMessage { @APIParam private String metadataContent; diff --git a/header/src/main/java/org/zstack/header/vm/APIResumeVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIResumeVmInstanceMsg.java index d86785d21bd..e646fb451e4 100755 --- a/header/src/main/java/org/zstack/header/vm/APIResumeVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIResumeVmInstanceMsg.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.MetadataImpact; /** * Created by root on 11/2/16. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIResumeVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIResumeVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java index e9e3cb26c33..07d576c2fae 100644 --- a/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIScanVmInstanceMetadataMsg.java @@ -7,12 +7,14 @@ import org.zstack.header.storage.primary.PrimaryStorageVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/metadata/scan", method = HttpMethod.GET, responseClass = APIScanVmInstanceMetadataReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIScanVmInstanceMetadataMsg extends APISyncCallMessage { @APIParam(required = false, resourceType = PrimaryStorageVO.class) private List primaryStorageUuids; 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..2086934a4db 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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ isAction = true, responseClass = APISetVmBootModeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APISetVmBootVolumeMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java index a72619161e0..625742db627 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.volume.VolumeVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2021/1/7. @@ -16,6 +17,7 @@ method = HttpMethod.PUT, responseClass = APISetVmBootVolumeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..c4aad1a636a 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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmClockTrackEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..1be1b75d6e3 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.RestRequest; import java.io.Serializable; +import org.zstack.header.vm.MetadataImpact; /** @@ -18,6 +19,7 @@ method = HttpMethod.PUT, responseClass = APISetVmConsolePasswordEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APISetVmDnsMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmDnsMsg.java index d1516fe179c..3874b66ecff 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmDnsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmDnsMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/{vmInstanceUuid}/actions", @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APISetVmDnsEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISetVmDnsMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; 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..0a37175bb5e 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.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -14,6 +15,7 @@ isAction = true, responseClass = APISetVmHostnameEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..5e1b5a0eda7 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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmQxlMemoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..0fdb777625d 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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmSoundTypeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..08a0ad53865 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.MetadataImpact; /** * Created by luchukun on 8/4/16. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APISetVmSshKeyEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..4bd6a7f0086 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.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APISetVmStaticIpEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APIStartVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIStartVmInstanceMsg.java index 11540fb9e42..e516aef6ca4 100755 --- a/header/src/main/java/org/zstack/header/vm/APIStartVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIStartVmInstanceMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api start a vm instance @@ -39,6 +40,7 @@ isAction = true ) @SkipVmTracer(replyClass = APIStartVmInstanceEvent.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIStartVmInstanceMsg extends APIMessage implements VmInstanceMessage, CheckAttachedVolumesMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIStopVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIStopVmInstanceMsg.java index a99e36446a0..fd5d5a47f2c 100755 --- a/header/src/main/java/org/zstack/header/vm/APIStopVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIStopVmInstanceMsg.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.MetadataImpact; /** * @api stop a vm instance @@ -36,6 +37,7 @@ responseClass = APIStopVmInstanceEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIStopVmInstanceMsg extends APIMessage implements VmInstanceMessage, StopVmMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotMsg.java b/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotMsg.java index 30439fac2de..387f3e0edab 100644 --- a/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotMsg.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.MetadataImpact; /** * @author shanshan.ning @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APITakeVmConsoleScreenshotEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APITakeVmConsoleScreenshotMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdatePriorityConfigMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdatePriorityConfigMsg.java index ab84afaad33..5896378c815 100644 --- a/header/src/main/java/org/zstack/header/vm/APIUpdatePriorityConfigMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdatePriorityConfigMsg.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.MetadataImpact; /** * @ Author : yh.w @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdatePriorityConfigEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdatePriorityConfigMsg extends APIMessage { @APIParam(resourceType = VmPriorityConfigVO.class) diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateTemplatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateTemplatedVmInstanceMsg.java index f0c2811fb19..5d8ab3a2f6b 100644 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateTemplatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateTemplatedVmInstanceMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.utils.data.SizeUnit; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/templatedVmInstance/{uuid}/actions", @@ -14,6 +15,7 @@ isAction = true, responseClass = APIUpdateTemplatedVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateTemplatedVmInstanceMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = TemplatedVmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java index c04a6a59d23..8071513d609 100644 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmMetadataMsg.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.MetadataImpact; @RestRequest( path = "/vm-instances/{vmUuid}/metadata/actions", @@ -11,6 +12,7 @@ responseClass = APIUpdateVmMetadataEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVmMetadataMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmUuid; 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..a3ea265981f 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.MetadataImpact; /** * @ Author : yh.w @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVmNicDriverEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..f9e3f469fcc 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.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ isAction = true, responseClass = APIUpdateVmPriorityEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APICreateVmCdRomMsg.java b/header/src/main/java/org/zstack/header/vm/cdrom/APICreateVmCdRomMsg.java index 72f0c8e9d5a..a04f3585bc1 100644 --- a/header/src/main/java/org/zstack/header/vm/cdrom/APICreateVmCdRomMsg.java +++ b/header/src/main/java/org/zstack/header/vm/cdrom/APICreateVmCdRomMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.tag.TagResourceType; import org.zstack.header.vm.VmInstanceMessage; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.MetadataImpact; /** * Create by lining at 2018/12/29 @@ -22,6 +23,7 @@ responseClass = APICreateVmCdRomEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVmCdRomMsg extends APICreateMessage implements APIAuditor, VmInstanceMessage { @APIParam(maxLength = 255) private String name; diff --git a/header/src/main/java/org/zstack/header/vm/cdrom/APIQueryVmCdRomMsg.java b/header/src/main/java/org/zstack/header/vm/cdrom/APIQueryVmCdRomMsg.java index 3c41e0fc112..57a46681b3c 100644 --- a/header/src/main/java/org/zstack/header/vm/cdrom/APIQueryVmCdRomMsg.java +++ b/header/src/main/java/org/zstack/header/vm/cdrom/APIQueryVmCdRomMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.rest.RestRequest; import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Create by lining at 2018/12/26 @@ -17,6 +18,7 @@ responseClass = APIQueryVmCdRomReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmCdRomMsg extends APIQueryMessage { public static List __example__() { return asList("name=cd-1"); 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..0727c2f9c6b 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.MetadataImpact; /** * Create by lining at 2018/12/29 @@ -18,6 +19,7 @@ isAction = true, responseClass = APISetVmInstanceDefaultCdRomEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/vm/cdrom/APIUpdateVmCdRomMsg.java b/header/src/main/java/org/zstack/header/vm/cdrom/APIUpdateVmCdRomMsg.java index 927426e5e9a..2fe88f73d03 100644 --- a/header/src/main/java/org/zstack/header/vm/cdrom/APIUpdateVmCdRomMsg.java +++ b/header/src/main/java/org/zstack/header/vm/cdrom/APIUpdateVmCdRomMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.MetadataImpact; /** * Create by lining at 2018/12/30 @@ -16,6 +17,7 @@ isAction = true, responseClass = APIUpdateVmCdRomEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVmCdRomMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmCdRomVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataArchiveMsg.java b/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataArchiveMsg.java index c63db9dd793..330c9a60244 100644 --- a/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataArchiveMsg.java +++ b/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataArchiveMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by LiangHanYu on 2022/6/17 17:31 @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryVmInstanceResourceMetadataArchiveReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmInstanceResourceMetadataArchiveMsg extends APIQueryMessage { public static List __example__() { return asList("uuid=" + uuid()); diff --git a/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataGroupMsg.java b/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataGroupMsg.java index f6d53f463cd..9c56d49fee7 100644 --- a/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataGroupMsg.java +++ b/header/src/main/java/org/zstack/header/vm/devices/APIQueryVmInstanceResourceMetadataGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by LiangHanYu on 2022/6/20 18:03 @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryVmInstanceResourceMetadataGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmInstanceResourceMetadataGroupMsg extends APIQueryMessage { public static List __example__() { return asList("uuid=" + uuid()); diff --git a/header/src/main/java/org/zstack/header/vo/APIGetResourceNamesMsg.java b/header/src/main/java/org/zstack/header/vo/APIGetResourceNamesMsg.java index ff9ec727649..430851bf307 100755 --- a/header/src/main/java/org/zstack/header/vo/APIGetResourceNamesMsg.java +++ b/header/src/main/java/org/zstack/header/vo/APIGetResourceNamesMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/5/1. @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIGetResourceNamesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetResourceNamesMsg extends APISyncCallMessage { @APIParam(nonempty = true) private List uuids; diff --git a/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToHostMsg.java b/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToHostMsg.java index 6e332fda5c8..ebdb4c28bf4 100644 --- a/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToHostMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToHostMsg.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.MetadataImpact; @RestRequest( path = "/volumes/{volumeUuid}/hosts/{hostUuid}", @@ -12,6 +13,7 @@ responseClass = APIAttachDataVolumeToHostEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachDataVolumeToHostMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String volumeUuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIBackupDataVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIBackupDataVolumeMsg.java index 7209c39583f..a73d0e93f4e 100755 --- a/header/src/main/java/org/zstack/header/volume/APIBackupDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIBackupDataVolumeMsg.java @@ -3,6 +3,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.storage.backup.BackupStorageVO; +import org.zstack.header.vm.MetadataImpact; /** * @api backup data volume to a backup storage @@ -30,6 +31,7 @@ * @since 0.1.0 */ @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIBackupDataVolumeMsg extends APIMessage implements VolumeMessage { /** * @desc data volume uuid diff --git a/header/src/main/java/org/zstack/header/volume/APIBatchSyncVolumeSizeMsg.java b/header/src/main/java/org/zstack/header/volume/APIBatchSyncVolumeSizeMsg.java index dfb03d2cf0c..78eb5ac597b 100644 --- a/header/src/main/java/org/zstack/header/volume/APIBatchSyncVolumeSizeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIBatchSyncVolumeSizeMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/volumes/batch-sync-volumes", method = HttpMethod.POST, responseClass = APIBatchSyncVolumeSizeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIBatchSyncVolumeSizeMsg extends APISyncCallMessage { @APIParam(resourceType = ClusterVO.class) private String clusterUuid; 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..0cce04161dc 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.MetadataImpact; /** * @api change data volume state @@ -38,6 +39,7 @@ method = HttpMethod.PUT, responseClass = APIChangeVolumeStateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeVolumeStateMsg extends APIMessage implements VolumeMessage { /** * @desc data volume uuid diff --git a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeSnapshotMsg.java index b03dee62a3b..fe92611f95c 100755 --- a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeSnapshotMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.tag.TagResourceType; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @api create data volume from a volume snapshot @@ -46,6 +47,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDataVolumeFromVolumeSnapshotMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeTemplateMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeTemplateMsg.java index 84d6f3b95a2..9d5e899f8f1 100755 --- a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeTemplateMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeFromVolumeTemplateMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.tag.TagResourceType; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -21,6 +22,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 72) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDataVolumeFromVolumeTemplateMsg extends APICreateMessage implements APIAuditor, VolumeCreateMessage { @APIParam(resourceType = ImageVO.class) private String imageUuid; diff --git a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeMsg.java index 87c501b580c..3dcaf4a7194 100755 --- a/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateDataVolumeMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.tag.TagResourceType; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @api create a new data volume @@ -46,6 +47,7 @@ responseClass = APICreateDataVolumeEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDataVolumeMsg extends APICreateMessage implements APIAuditor, VolumeCreateMessage { /** * @desc max length of 255 characters 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..99136e702aa 100755 --- a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @api create a volume snapshot from volume @@ -45,6 +46,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APIDetachDataVolumeFromHostMsg.java b/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromHostMsg.java index 742addae83a..404e5f7b5fd 100644 --- a/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromHostMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromHostMsg.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.MetadataImpact; @RestRequest( path = "/volumes/{volumeUuid}/hosts", method = HttpMethod.DELETE, responseClass = APIDetachDataVolumeFromHostEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachDataVolumeFromHostMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String volumeUuid; 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..14f6c69f885 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.MetadataImpact; /** * Created by frank on 11/16/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIExpungeDataVolumeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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/APIGetDataVolumeAttachableVmMsg.java b/header/src/main/java/org/zstack/header/volume/APIGetDataVolumeAttachableVmMsg.java index fe2f0caa362..e872d19aef7 100755 --- a/header/src/main/java/org/zstack/header/volume/APIGetDataVolumeAttachableVmMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIGetDataVolumeAttachableVmMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetDataVolumeAttachableVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetDataVolumeAttachableVmMsg extends APISyncCallMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String volumeUuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIGetVolumeCapabilitiesMsg.java b/header/src/main/java/org/zstack/header/volume/APIGetVolumeCapabilitiesMsg.java index 75e52f1c4b6..c32c4b5f12c 100755 --- a/header/src/main/java/org/zstack/header/volume/APIGetVolumeCapabilitiesMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIGetVolumeCapabilitiesMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/5/19. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVolumeCapabilitiesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVolumeCapabilitiesMsg extends APISyncCallMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIGetVolumeFormatMsg.java b/header/src/main/java/org/zstack/header/volume/APIGetVolumeFormatMsg.java index b401d60ab6b..0a48cdd22fc 100755 --- a/header/src/main/java/org/zstack/header/volume/APIGetVolumeFormatMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIGetVolumeFormatMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -11,6 +12,7 @@ method = HttpMethod.GET, responseClass = APIGetVolumeFormatReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVolumeFormatMsg extends APISyncCallMessage { public static APIGetVolumeFormatMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/volume/APIQueryVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIQueryVolumeMsg.java index f97f77ae8ab..eed79908267 100755 --- a/header/src/main/java/org/zstack/header/volume/APIQueryVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIQueryVolumeMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVolumeReply.class, inventoryClass = VolumeInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryVolumeReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVolumeMsg extends APIQueryMessage { 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..1aebe825867 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.MetadataImpact; /** * Created by xing5 on 2016/4/24. @@ -14,6 +15,7 @@ responseClass = APISyncVolumeSizeEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..216d8f2cba5 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.MetadataImpact; /** * @ Author : yh.w @@ -16,6 +17,7 @@ method = HttpMethod.PUT, responseClass = APIUndoSnapshotCreationEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) 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..434ab40b9f5 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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdateVolumeEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVolumeMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/zone/APIChangeZoneStateMsg.java b/header/src/main/java/org/zstack/header/zone/APIChangeZoneStateMsg.java index 76a8335fca7..d30ef74cfe9 100755 --- a/header/src/main/java/org/zstack/header/zone/APIChangeZoneStateMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APIChangeZoneStateMsg.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.MetadataImpact; /** @@ -46,6 +47,7 @@ responseClass = APIChangeZoneStateEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeZoneStateMsg extends APIMessage implements ZoneMessage { /** * @desc zone uuid diff --git a/header/src/main/java/org/zstack/header/zone/APICreateZoneMsg.java b/header/src/main/java/org/zstack/header/zone/APICreateZoneMsg.java index e1fa32409aa..d057f9355f0 100755 --- a/header/src/main/java/org/zstack/header/zone/APICreateZoneMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APICreateZoneMsg.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.MetadataImpact; /** * @api create a new zone @@ -42,6 +43,7 @@ parameterName = "params", responseClass = APICreateZoneEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateZoneMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/header/src/main/java/org/zstack/header/zone/APIDeleteZoneMsg.java b/header/src/main/java/org/zstack/header/zone/APIDeleteZoneMsg.java index df1420eaccc..7b34e3d9f00 100755 --- a/header/src/main/java/org/zstack/header/zone/APIDeleteZoneMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APIDeleteZoneMsg.java @@ -10,6 +10,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api delete a zone. All descendant resources, for example cluster/host/vm, are deleted in @@ -43,6 +44,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteZoneEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteZoneMsg extends APIDeleteMessage implements ZoneMessage { /** * @desc zone uuid diff --git a/header/src/main/java/org/zstack/header/zone/APIGetZoneMsg.java b/header/src/main/java/org/zstack/header/zone/APIGetZoneMsg.java index 921ba77ed5f..076fa4924cc 100755 --- a/header/src/main/java/org/zstack/header/zone/APIGetZoneMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APIGetZoneMsg.java @@ -3,12 +3,14 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/zones/{uuid}/info", method = HttpMethod.GET, responseClass = APIGetZoneReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetZoneMsg extends APISyncCallMessage { private String uuid; diff --git a/header/src/main/java/org/zstack/header/zone/APIQueryZoneMsg.java b/header/src/main/java/org/zstack/header/zone/APIQueryZoneMsg.java index 047835f7a9e..27adc53c871 100755 --- a/header/src/main/java/org/zstack/header/zone/APIQueryZoneMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APIQueryZoneMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryZoneReply.class, inventoryClass = ZoneInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryZoneReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryZoneMsg extends APIQueryMessage { public static List __example__() { diff --git a/header/src/main/java/org/zstack/header/zone/APIUpdateZoneMsg.java b/header/src/main/java/org/zstack/header/zone/APIUpdateZoneMsg.java index d2718cc007b..65e0e6c3559 100755 --- a/header/src/main/java/org/zstack/header/zone/APIUpdateZoneMsg.java +++ b/header/src/main/java/org/zstack/header/zone/APIUpdateZoneMsg.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.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ responseClass = APIUpdateZoneEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateZoneMsg extends APIMessage implements ZoneMessage { @APIParam(maxLength = 255, required = false) private String name; diff --git a/plugin/account-import/src/main/java/org/zstack/identity/imports/api/APIQueryThirdPartyAccountSourceBindingMsg.java b/plugin/account-import/src/main/java/org/zstack/identity/imports/api/APIQueryThirdPartyAccountSourceBindingMsg.java index 1813701c2fd..41784cbe7a2 100644 --- a/plugin/account-import/src/main/java/org/zstack/identity/imports/api/APIQueryThirdPartyAccountSourceBindingMsg.java +++ b/plugin/account-import/src/main/java/org/zstack/identity/imports/api/APIQueryThirdPartyAccountSourceBindingMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryThirdPartyAccountSourceBindingReply.class, inventoryClass = AccountThirdPartyAccountSourceRefInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQueryThirdPartyAccountSourceBindingReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryThirdPartyAccountSourceBindingMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListEntryMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListEntryMsg.java index 65566a737ed..c6e4042b9f0 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListEntryMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListEntryMsg.java @@ -1,66 +1,68 @@ -package org.zstack.header.acl; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APICreateMessage; -import org.zstack.header.message.APIEvent; -import org.zstack.header.message.APIMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.other.APIAuditor; -import org.zstack.header.rest.RestRequest; - -/** - * @author: zhanyong.miao - * @date: 2020-03-09 - **/ -@RestRequest( - path = "/access-control-lists/{aclUuid}/ipentries", - method = HttpMethod.POST, - responseClass = APIAddAccessControlListEntryEvent.class, - parameterName = "params" -) -public class APIAddAccessControlListEntryMsg extends APICreateMessage implements APIAuditor { - @APIParam(resourceType = AccessControlListVO.class) - private String aclUuid; - @APIParam(maxLength = 2048) - private String entries; - @APIParam(maxLength = 2048, required = false) - private String description; - - public String getAclUuid() { - return aclUuid; - } - - public void setAclUuid(String aclUuid) { - this.aclUuid = aclUuid; - } - - public String getEntries() { - return entries; - } - - public void setEntries(String entries) { - this.entries = entries; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public static APIAddAccessControlListEntryMsg __example__() { - APIAddAccessControlListEntryMsg msg = new APIAddAccessControlListEntryMsg(); - - msg.setAclUuid(uuid(AccessControlListVO.class)); - msg.setEntries("192.168.12.1,192.168.48.0/24"); - - return msg; - } - - @Override - public Result audit(APIMessage msg, APIEvent rsp) { - return new Result(rsp.isSuccess() ? ((APIAddAccessControlListEntryEvent)rsp).getInventory().getUuid() : "", AccessControlListEntryVO.class); - } -} +package org.zstack.header.acl; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-09 + **/ +@RestRequest( + path = "/access-control-lists/{aclUuid}/ipentries", + method = HttpMethod.POST, + responseClass = APIAddAccessControlListEntryEvent.class, + parameterName = "params" +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIAddAccessControlListEntryMsg extends APICreateMessage implements APIAuditor { + @APIParam(resourceType = AccessControlListVO.class) + private String aclUuid; + @APIParam(maxLength = 2048) + private String entries; + @APIParam(maxLength = 2048, required = false) + private String description; + + public String getAclUuid() { + return aclUuid; + } + + public void setAclUuid(String aclUuid) { + this.aclUuid = aclUuid; + } + + public String getEntries() { + return entries; + } + + public void setEntries(String entries) { + this.entries = entries; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public static APIAddAccessControlListEntryMsg __example__() { + APIAddAccessControlListEntryMsg msg = new APIAddAccessControlListEntryMsg(); + + msg.setAclUuid(uuid(AccessControlListVO.class)); + msg.setEntries("192.168.12.1,192.168.48.0/24"); + + return msg; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new Result(rsp.isSuccess() ? ((APIAddAccessControlListEntryEvent)rsp).getInventory().getUuid() : "", AccessControlListEntryVO.class); + } +} diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListRedirectRuleMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListRedirectRuleMsg.java index bfd0b6b307e..0980849a46a 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListRedirectRuleMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIAddAccessControlListRedirectRuleMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(AccessControlListVO.class) @RestRequest( @@ -19,6 +20,7 @@ responseClass = APIAddAccessControlListEntryEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddAccessControlListRedirectRuleMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255, required = false) private String name; diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIChangeAccessControlListRedirectRuleMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIChangeAccessControlListRedirectRuleMsg.java index 8b4259286a7..66fca508227 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIChangeAccessControlListRedirectRuleMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIChangeAccessControlListRedirectRuleMsg.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.MetadataImpact; @RestRequest( path = "/access-control-lists/redirectRules/{uuid}/actions", @@ -13,6 +14,7 @@ responseClass = APIChangeAccessControlListRedirectRuleEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeAccessControlListRedirectRuleMsg extends APIMessage implements APIAuditor { @APIParam(resourceType = AccessControlListEntryVO.class) private String uuid; diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APICreateAccessControlListMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APICreateAccessControlListMsg.java index b9bd6779c32..661251ba287 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APICreateAccessControlListMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APICreateAccessControlListMsg.java @@ -1,69 +1,71 @@ -package org.zstack.header.acl; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APICreateMessage; -import org.zstack.header.message.APIEvent; -import org.zstack.header.message.APIMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.other.APIAuditor; -import org.zstack.header.rest.RestRequest; -import org.zstack.header.tag.TagResourceType; - -/** - * @author: zhanyong.miao - * @date: 2020-03-09 - **/ -@TagResourceType(AccessControlListVO.class) -@RestRequest( - path = "/access-control-lists", - method = HttpMethod.POST, - responseClass = APICreateAccessControlListEvent.class, - parameterName = "params" -) -public class APICreateAccessControlListMsg extends APICreateMessage implements APIAuditor { - @APIParam(maxLength = 255) - private String name; - @APIParam(maxLength = 2048, required = false) - private String description; - @APIParam(validValues = {"4", "6"}, required = false) - private Integer ipVersion; - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Integer getIpVersion() { - return ipVersion; - } - - public void setIpVersion(Integer ipVersion) { - this.ipVersion = ipVersion; - } - - public static APICreateAccessControlListMsg __example__() { - APICreateAccessControlListMsg msg = new APICreateAccessControlListMsg(); - - msg.setName("acl-group"); - msg.setIpVersion(4); - - return msg; - } - - @Override - public Result audit(APIMessage msg, APIEvent rsp) { - return new Result(rsp.isSuccess() ? ((APICreateAccessControlListEvent)rsp).getInventory().getUuid() : "", AccessControlListVO.class); - } -} +package org.zstack.header.acl; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-09 + **/ +@TagResourceType(AccessControlListVO.class) +@RestRequest( + path = "/access-control-lists", + method = HttpMethod.POST, + responseClass = APICreateAccessControlListEvent.class, + parameterName = "params" +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APICreateAccessControlListMsg extends APICreateMessage implements APIAuditor { + @APIParam(maxLength = 255) + private String name; + @APIParam(maxLength = 2048, required = false) + private String description; + @APIParam(validValues = {"4", "6"}, required = false) + private Integer ipVersion; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getIpVersion() { + return ipVersion; + } + + public void setIpVersion(Integer ipVersion) { + this.ipVersion = ipVersion; + } + + public static APICreateAccessControlListMsg __example__() { + APICreateAccessControlListMsg msg = new APICreateAccessControlListMsg(); + + msg.setName("acl-group"); + msg.setIpVersion(4); + + return msg; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new Result(rsp.isSuccess() ? ((APICreateAccessControlListEvent)rsp).getInventory().getUuid() : "", AccessControlListVO.class); + } +} diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIDeleteAccessControlListMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIDeleteAccessControlListMsg.java index c919063f71e..09cadf92dc2 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIDeleteAccessControlListMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIDeleteAccessControlListMsg.java @@ -1,34 +1,36 @@ -package org.zstack.header.acl; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APIDeleteMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.rest.RestRequest; - -/** - * @author: zhanyong.miao - * @date: 2020-03-09 - **/ -@RestRequest( - path = "/access-control-lists/{uuid}", - method = HttpMethod.DELETE, - responseClass = APIDeleteAccessControlListEvent.class -) -public class APIDeleteAccessControlListMsg extends APIDeleteMessage { - @APIParam(resourceType = AccessControlListVO.class, successIfResourceNotExisting = true) - private String uuid; - - public String getUuid() { - return uuid; - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public static APIDeleteAccessControlListMsg __example__() { - APIDeleteAccessControlListMsg msg = new APIDeleteAccessControlListMsg(); - msg.setUuid(uuid()); - return msg; - } -} +package org.zstack.header.acl; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-09 + **/ +@RestRequest( + path = "/access-control-lists/{uuid}", + method = HttpMethod.DELETE, + responseClass = APIDeleteAccessControlListEvent.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIDeleteAccessControlListMsg extends APIDeleteMessage { + @APIParam(resourceType = AccessControlListVO.class, successIfResourceNotExisting = true) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public static APIDeleteAccessControlListMsg __example__() { + APIDeleteAccessControlListMsg msg = new APIDeleteAccessControlListMsg(); + msg.setUuid(uuid()); + return msg; + } +} diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIQueryAccessControlListMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIQueryAccessControlListMsg.java index 94abeb2801f..8f5904ba4e2 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIQueryAccessControlListMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIQueryAccessControlListMsg.java @@ -1,29 +1,31 @@ -package org.zstack.header.acl; - -import org.springframework.http.HttpMethod; -import org.zstack.header.query.APIQueryMessage; -import org.zstack.header.query.AutoQuery; -import org.zstack.header.rest.RestRequest; - -import java.util.List; - -import static java.util.Arrays.asList; - -/** - * @author: zhanyong.miao - * @date: 2020-03-09 - **/ -@AutoQuery(replyClass = APIQueryAccessControlListReply.class, inventoryClass = AccessControlListInventory.class) -@RestRequest( - path = "/access-control-lists", - optionalPaths = {"/access-control-lists/{uuid}"}, - method = HttpMethod.GET, - responseClass = APIQueryAccessControlListReply.class -) -public class APIQueryAccessControlListMsg extends APIQueryMessage { - - public static List __example__() { - return asList(); - } - +package org.zstack.header.acl; + +import org.springframework.http.HttpMethod; +import org.zstack.header.query.APIQueryMessage; +import org.zstack.header.query.AutoQuery; +import org.zstack.header.rest.RestRequest; + +import java.util.List; + +import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-09 + **/ +@AutoQuery(replyClass = APIQueryAccessControlListReply.class, inventoryClass = AccessControlListInventory.class) +@RestRequest( + path = "/access-control-lists", + optionalPaths = {"/access-control-lists/{uuid}"}, + method = HttpMethod.GET, + responseClass = APIQueryAccessControlListReply.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIQueryAccessControlListMsg extends APIQueryMessage { + + public static List __example__() { + return asList(); + } + } \ No newline at end of file diff --git a/plugin/acl/src/main/java/org/zstack/header/acl/APIRemoveAccessControlListEntryMsg.java b/plugin/acl/src/main/java/org/zstack/header/acl/APIRemoveAccessControlListEntryMsg.java index c9e614720c7..bfae47f5a76 100644 --- a/plugin/acl/src/main/java/org/zstack/header/acl/APIRemoveAccessControlListEntryMsg.java +++ b/plugin/acl/src/main/java/org/zstack/header/acl/APIRemoveAccessControlListEntryMsg.java @@ -1,54 +1,56 @@ -package org.zstack.header.acl; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APIDeleteMessage; -import org.zstack.header.message.APIEvent; -import org.zstack.header.message.APIMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.other.APIAuditor; -import org.zstack.header.rest.RestRequest; - -/** - * @author: zhanyong.miao - * @date: 2020-03-09 - **/ -@RestRequest( - path = "/access-control-lists/{aclUuid}/ipentries/{uuid}", - method = HttpMethod.DELETE, - responseClass = APIRemoveAccessControlListEntryEvent.class -) -public class APIRemoveAccessControlListEntryMsg extends APIDeleteMessage implements APIAuditor{ - @APIParam(resourceType = AccessControlListVO.class) - private String aclUuid; - @APIParam(resourceType = AccessControlListEntryVO.class, successIfResourceNotExisting = true) - private String uuid; - - public String getAclUuid() { - return aclUuid; - } - - public void setAclUuid(String aclUuid) { - this.aclUuid = aclUuid; - } - - public String getUuid() { - return uuid; - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public static APIRemoveAccessControlListEntryMsg __example__() { - APIRemoveAccessControlListEntryMsg msg = new APIRemoveAccessControlListEntryMsg(); - msg.setUuid(uuid()); - msg.setAclUuid(uuid()); - - return msg; - } - - @Override - public APIAuditor.Result audit(APIMessage msg, APIEvent rsp) { - return new APIAuditor.Result(((APIRemoveAccessControlListEntryMsg)msg).getUuid(), AccessControlListEntryVO.class); - } -} +package org.zstack.header.acl; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-09 + **/ +@RestRequest( + path = "/access-control-lists/{aclUuid}/ipentries/{uuid}", + method = HttpMethod.DELETE, + responseClass = APIRemoveAccessControlListEntryEvent.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIRemoveAccessControlListEntryMsg extends APIDeleteMessage implements APIAuditor{ + @APIParam(resourceType = AccessControlListVO.class) + private String aclUuid; + @APIParam(resourceType = AccessControlListEntryVO.class, successIfResourceNotExisting = true) + private String uuid; + + public String getAclUuid() { + return aclUuid; + } + + public void setAclUuid(String aclUuid) { + this.aclUuid = aclUuid; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public static APIRemoveAccessControlListEntryMsg __example__() { + APIRemoveAccessControlListEntryMsg msg = new APIRemoveAccessControlListEntryMsg(); + msg.setUuid(uuid()); + msg.setAclUuid(uuid()); + + return msg; + } + + @Override + public APIAuditor.Result audit(APIMessage msg, APIEvent rsp) { + return new APIAuditor.Result(((APIRemoveAccessControlListEntryMsg)msg).getUuid(), AccessControlListEntryVO.class); + } +} diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/APIQueryApplianceVmMsg.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/APIQueryApplianceVmMsg.java index 9a21bcae536..3fcd502c57d 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/APIQueryApplianceVmMsg.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/APIQueryApplianceVmMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryApplianceVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryApplianceVmMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddCephBackupStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddCephBackupStorageMsg.java index e7e2795e657..41f6b8fd2fa 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddCephBackupStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddCephBackupStorageMsg.java @@ -15,6 +15,7 @@ import java.io.Serializable; import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/27/2015. @@ -29,6 +30,7 @@ parameterName = "params", responseClass = APIAddBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddCephBackupStorageMsg extends APIAddBackupStorageMsg implements Serializable { @APIParam(nonempty = false, emptyString = false) @NoLogging(type = NoLogging.Type.Uri) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddMonToCephBackupStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddMonToCephBackupStorageMsg.java index 556bc277028..03b975196d8 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddMonToCephBackupStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIAddMonToCephBackupStorageMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/1/2015. @@ -21,6 +22,7 @@ parameterName = "params", responseClass = APIAddMonToCephBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddMonToCephBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = CephBackupStorageVO.class) private String uuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIQueryCephBackupStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIQueryCephBackupStorageMsg.java index a4e8c4eedf7..03f6cc0fd99 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIQueryCephBackupStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIQueryCephBackupStorageMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/6/2015. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryBackupStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryCephBackupStorageMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIRemoveMonFromCephBackupStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIRemoveMonFromCephBackupStorageMsg.java index fd45ed139a0..94e5c42a022 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIRemoveMonFromCephBackupStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIRemoveMonFromCephBackupStorageMsg.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/1/2015. @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveMonFromCephBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveMonFromCephBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = CephBackupStorageVO.class) private String uuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIUpdateCephBackupStorageMonMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIUpdateCephBackupStorageMonMsg.java index d29ad15dcbc..a954cd0d20d 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIUpdateCephBackupStorageMonMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/APIUpdateCephBackupStorageMonMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.storage.backup.BackupStorageMessage; import java.io.Serializable; +import org.zstack.header.vm.MetadataImpact; /** * Created by Mei Lei on 6/3/2016. @@ -19,6 +20,7 @@ isAction = true, responseClass = APIUpdateCephBackupStorageMonEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateCephBackupStorageMonMsg extends APIMessage implements BackupStorageMessage, Serializable { @APINoSee private String backupStorageUuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStorageMsg.java index fd7f7590f15..70562098f23 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStorageMsg.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/28/2015. @@ -26,6 +27,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddCephPrimaryStorageMsg extends APIAddPrimaryStorageMsg { @APIParam(nonempty = false, emptyString = false) @NoLogging(type = NoLogging.Type.Uri) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStoragePoolMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStoragePoolMsg.java index 458a3060d51..396a114c6c3 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStoragePoolMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddCephPrimaryStoragePoolMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/2/28. @@ -19,6 +20,7 @@ parameterName = "params", responseClass = APIAddCephPrimaryStoragePoolEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddCephPrimaryStoragePoolMsg extends APICreateMessage implements PrimaryStorageMessage, APIAuditor { @APIParam(resourceType = PrimaryStorageVO.class) private String primaryStorageUuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddMonToCephPrimaryStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddMonToCephPrimaryStorageMsg.java index 3d91a19e274..ffd0750480a 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddMonToCephPrimaryStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIAddMonToCephPrimaryStorageMsg.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/6/2015. @@ -21,6 +22,7 @@ responseClass = APIAddMonToCephPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddMonToCephPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = CephPrimaryStorageVO.class) private String uuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIDeleteCephPrimaryStoragePoolMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIDeleteCephPrimaryStoragePoolMsg.java index 7a56364cede..54424c02a2b 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIDeleteCephPrimaryStoragePoolMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIDeleteCephPrimaryStoragePoolMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/2/28. @@ -18,6 +19,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteCephPrimaryStoragePoolEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteCephPrimaryStoragePoolMsg extends APIMessage implements PrimaryStorageMessage, APIAuditor { @APIParam(resourceType = CephPrimaryStoragePoolVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephOsdGroupMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephOsdGroupMsg.java index c4d995c7ebd..f61c58d4c51 100644 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephOsdGroupMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephOsdGroupMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/ceph/osdgroups", @@ -15,6 +16,7 @@ responseClass = APIQueryCephOsdGroupReply.class ) @AutoQuery(replyClass = APIQueryCephOsdGroupReply.class, inventoryClass = CephOsdGroupInventory.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryCephOsdGroupMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStorageMsg.java index 92095b60416..c1fb83d88bb 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStorageMsg.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/6/2015. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryPrimaryStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryCephPrimaryStorageMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStoragePoolMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStoragePoolMsg.java index 3e03560f89d..ee37bac1046 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStoragePoolMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIQueryCephPrimaryStoragePoolMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2017/2/28. @@ -19,6 +20,7 @@ responseClass = APIQueryCephPrimaryStoragePoolReply.class ) @AutoQuery(replyClass = APIQueryCephPrimaryStoragePoolReply.class, inventoryClass = CephPrimaryStoragePoolInventory.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryCephPrimaryStoragePoolMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIRemoveMonFromCephPrimaryStorageMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIRemoveMonFromCephPrimaryStorageMsg.java index ec8cccc3d03..3d0c1215b2d 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIRemoveMonFromCephPrimaryStorageMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIRemoveMonFromCephPrimaryStorageMsg.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/6/2015. @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveMonFromCephPrimaryStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveMonFromCephPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStorageMonMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStorageMonMsg.java index e77b5fe8fca..0a9041fa341 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStorageMonMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStorageMonMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.storage.primary.PrimaryStorageMessage; import java.io.Serializable; +import org.zstack.header.vm.MetadataImpact; /** * Created by Mei Lei on 6/6/2016. @@ -19,6 +20,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateCephPrimaryStorageMonEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateCephPrimaryStorageMonMsg extends APIMessage implements PrimaryStorageMessage, Serializable { @APINoSee private String primaryStorageUuid; diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStoragePoolMsg.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStoragePoolMsg.java index 133baa5aee5..7ac9f9a6fde 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStoragePoolMsg.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/APIUpdateCephPrimaryStoragePoolMsg.java @@ -7,6 +7,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.MetadataImpact; /** * Created by AlanJager on 2017/9/4. @@ -17,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateCephPrimaryStoragePoolEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateCephPrimaryStoragePoolMsg extends APIMessage implements PrimaryStorageMessage { @APINoSee private String primaryStorageUuid; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIAddResourcesToDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIAddResourcesToDirectoryMsg.java index 0c8728555b1..98b873f482a 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIAddResourcesToDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIAddResourcesToDirectoryMsg.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @author shenjin @@ -21,6 +22,7 @@ responseClass = APIAddResourcesToDirectoryEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddResourcesToDirectoryMsg extends APIMessage implements DirectoryMessage { @APIParam(resourceType = ResourceVO.class, nonempty = true) private List resourceUuids; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APICreateDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APICreateDirectoryMsg.java index fc2dfefddf0..a4aacc96022 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APICreateDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APICreateDirectoryMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; /** * @author shenjin @@ -16,6 +17,7 @@ responseClass = APICreateDirectoryEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateDirectoryMsg extends APICreateMessage implements OperateDirectoryMessage { @APIParam(maxLength = 255) private String name; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIDeleteDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIDeleteDirectoryMsg.java index ba650511b47..80a63bf88ad 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIDeleteDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIDeleteDirectoryMsg.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.MetadataImpact; /** * @author shenjin @@ -14,6 +15,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteDirectoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteDirectoryMsg extends APIDeleteMessage implements DirectoryMessage, OperateDirectoryMessage { @APIParam(resourceType = DirectoryVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIMoveDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIMoveDirectoryMsg.java index c850db04ec5..d0f45b2f916 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIMoveDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIMoveDirectoryMsg.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.MetadataImpact; /** * @author shenjin @@ -16,6 +17,7 @@ isAction = true, responseClass = APIMoveDirectoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIMoveDirectoryMsg extends APIMessage implements DirectoryMessage, OperateDirectoryMessage { @APIParam(resourceType = DirectoryVO.class) private String targetParentUuid; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIMoveResourcesToDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIMoveResourcesToDirectoryMsg.java index 20ccb1ad421..efc5c214fa9 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIMoveResourcesToDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIMoveResourcesToDirectoryMsg.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @author shenjin @@ -20,6 +21,7 @@ isAction = true, responseClass = APIMoveResourcesToDirectoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIMoveResourcesToDirectoryMsg extends APIMessage implements DirectoryMessage { @APIParam(resourceType = ResourceVO.class, nonempty = true) private List resourceUuids; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIQueryDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIQueryDirectoryMsg.java index 98eb042d0e2..1c487008417 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIQueryDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIQueryDirectoryMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * @author shenjin @@ -20,6 +21,7 @@ method = HttpMethod.GET, responseClass = APIQueryDirectoryReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryDirectoryMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIRemoveResourcesFromDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIRemoveResourcesFromDirectoryMsg.java index 4adf9ac55a2..91a6966cdf8 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIRemoveResourcesFromDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIRemoveResourcesFromDirectoryMsg.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @author shenjin @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveResourcesFromDirectoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveResourcesFromDirectoryMsg extends APIMessage implements DirectoryMessage{ @APIParam(resourceType = ResourceVO.class, nonempty = true) private List resourceUuids; diff --git a/plugin/directory/src/main/java/org/zstack/directory/APIUpdateDirectoryMsg.java b/plugin/directory/src/main/java/org/zstack/directory/APIUpdateDirectoryMsg.java index 40ef5220240..4dc970503c9 100644 --- a/plugin/directory/src/main/java/org/zstack/directory/APIUpdateDirectoryMsg.java +++ b/plugin/directory/src/main/java/org/zstack/directory/APIUpdateDirectoryMsg.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.MetadataImpact; /** * @author shenjin @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateDirectoryEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateDirectoryMsg extends APIMessage implements DirectoryMessage, OperateDirectoryMessage { @APIParam(resourceType = DirectoryVO.class) private String uuid; diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIAttachEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIAttachEipMsg.java index db5e4c66f4c..bcd95b5a1a3 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIAttachEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIAttachEipMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.UsedIpVO; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -52,6 +53,7 @@ parameterName = "params", responseClass = APIAttachEipEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachEipMsg extends APIMessage implements EipMessage { /** * @desc eip uuid diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIChangeEipStateMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIChangeEipStateMsg.java index d69000dfd33..7f3c1c42bcd 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIChangeEipStateMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIChangeEipStateMsg.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.MetadataImpact; /** */ @@ -13,6 +14,7 @@ responseClass = APIChangeEipStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeEipStateMsg extends APIMessage implements EipMessage { @APIParam(resourceType = EipVO.class) private String uuid; diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APICreateEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APICreateEipMsg.java index c6f2b1892f7..1c4d0256749 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APICreateEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APICreateEipMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; import org.zstack.network.service.vip.VipVO; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -61,6 +62,7 @@ responseClass = APICreateEipEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateEipMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDeleteEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDeleteEipMsg.java index bdbc1716caf..5c84cebdfce 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDeleteEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDeleteEipMsg.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.MetadataImpact; /** * @api @@ -48,6 +49,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteEipEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteEipMsg extends APIDeleteMessage implements EipMessage { /** * @desc eip uuid diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDetachEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDetachEipMsg.java index 9e9e395efcd..5a2a00ba01d 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDetachEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIDetachEipMsg.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -54,6 +55,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachEipEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachEipMsg extends APIMessage implements EipMessage, APIMultiAuditor { /** * @desc eip uuid diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetEipAttachableVmNicsMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetEipAttachableVmNicsMsg.java index 0071a56552b..9564126812b 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetEipAttachableVmNicsMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetEipAttachableVmNicsMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIGetMessage; import org.zstack.header.rest.RestRequest; import org.zstack.network.service.vip.VipVO; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetEipAttachableVmNicsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetEipAttachableVmNicsMsg extends APIGetMessage { @APIParam(required = false, resourceType = EipVO.class) private String eipUuid; diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetVmNicAttachableEipsMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetVmNicAttachableEipsMsg.java index 2411397d534..5f353e034e2 100644 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetVmNicAttachableEipsMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIGetVmNicAttachableEipsMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.*; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/nics/{vmNicUuid}/candidate-eips", method = HttpMethod.GET, responseClass = APIGetVmNicAttachableEipsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVmNicAttachableEipsMsg extends APIGetMessage { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIQueryEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIQueryEipMsg.java index ebd46730ba8..97d1eaebc2b 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIQueryEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIQueryEipMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQueryEipReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryEipMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIUpdateEipMsg.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIUpdateEipMsg.java index 8ce5d76a833..bf489df5139 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/APIUpdateEipMsg.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/APIUpdateEipMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIUpdateEipEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateEipMsg extends APIMessage implements EipMessage { @APIParam(resourceType = EipVO.class) private String uuid; diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIChangeL3NetworkDhcpIpAddressMsg.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIChangeL3NetworkDhcpIpAddressMsg.java index 5161c3c4fbf..6c486bcdec1 100644 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIChangeL3NetworkDhcpIpAddressMsg.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIChangeL3NetworkDhcpIpAddressMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.network.l3.L3NetworkMessage; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @Action(category = L3NetworkConstant.ACTION_CATEGORY) @RestRequest( @@ -16,6 +17,7 @@ isAction = true, responseClass = APIChangeL3NetworkDhcpIpAddressEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeL3NetworkDhcpIpAddressMsg extends APIMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class, checkAccount = true, operationTarget = true) private String l3NetworkUuid; diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkDhcpIpAddressMsg.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkDhcpIpAddressMsg.java index b379076b91a..f22577a3e63 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkDhcpIpAddressMsg.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkDhcpIpAddressMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkMessage; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by miao on 16-7-19. @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIGetL3NetworkDhcpIpAddressReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL3NetworkDhcpIpAddressMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkIpStatisticMsg.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkIpStatisticMsg.java index 246e9912f97..b6ff0831f6f 100644 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkIpStatisticMsg.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/APIGetL3NetworkIpStatisticMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import static org.zstack.network.service.flat.IpStatisticConstants.*; +import org.zstack.header.vm.MetadataImpact; /** * Created by Qi Le on 2019/9/9 @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIGetL3NetworkIpStatisticReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetL3NetworkIpStatisticMsg extends APISyncCallMessage implements L3NetworkMessage { @APIParam(resourceType = L3NetworkVO.class) private String l3NetworkUuid; diff --git a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIChangeHostNetworkInterfaceLldpModeMsg.java b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIChangeHostNetworkInterfaceLldpModeMsg.java index 7ae12a108b6..31a37c1d982 100644 --- a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIChangeHostNetworkInterfaceLldpModeMsg.java +++ b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIChangeHostNetworkInterfaceLldpModeMsg.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hostNetworkInterface/lldp/actions", @@ -17,6 +18,7 @@ responseClass = APIChangeHostNetworkInterfaceLldpModeEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeHostNetworkInterfaceLldpModeMsg extends APIMessage implements APIAuditor { @APIParam(resourceType = HostNetworkInterfaceVO.class) private List interfaceUuids; diff --git a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIGetHostNetworkInterfaceLldpMsg.java b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIGetHostNetworkInterfaceLldpMsg.java index c6df5bfcf1a..9359653413c 100644 --- a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIGetHostNetworkInterfaceLldpMsg.java +++ b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIGetHostNetworkInterfaceLldpMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.network.hostNetworkInterface.HostNetworkInterfaceVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hostNetworkInterface/lldp/{interfaceUuid}/info", method = HttpMethod.GET, responseClass = APIGetHostNetworkInterfaceLldpReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetHostNetworkInterfaceLldpMsg extends APISyncCallMessage { @APIParam(resourceType = HostNetworkInterfaceVO.class) private String interfaceUuid; diff --git a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIQueryHostNetworkInterfaceLldpMsg.java b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIQueryHostNetworkInterfaceLldpMsg.java index 14b999bee6c..667a9365946 100644 --- a/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIQueryHostNetworkInterfaceLldpMsg.java +++ b/plugin/hostNetworkInterface/src/main/java/org/zstack/network/hostNetworkInterface/lldp/api/APIQueryHostNetworkInterfaceLldpMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryHostNetworkInterfaceLldpReply.class, inventoryClass = HostNetworkInterfaceLldpInventory.class) @RestRequest( @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryHostNetworkInterfaceLldpReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryHostNetworkInterfaceLldpMsg extends APIQueryMessage { public static List __example__() { return asList(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/APIAddKVMHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/APIAddKVMHostMsg.java index c41b987e323..2ce9cd43c26 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/APIAddKVMHostMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/APIAddKVMHostMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.tag.TagResourceType; import java.util.concurrent.TimeUnit; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -68,6 +69,7 @@ responseClass = APIAddHostEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddKVMHostMsg extends APIAddHostMsg implements AddKVMHostMessage { /** * @desc user name used for ssh login. diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/APIKvmRunShellMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/APIKvmRunShellMsg.java index 97a48f0fac4..8b99c5a097c 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/APIKvmRunShellMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/APIKvmRunShellMsg.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.Set; +import org.zstack.header.vm.MetadataImpact; @@ -20,6 +21,7 @@ method = HttpMethod.PUT, responseClass = APIKvmRunShellEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIKvmRunShellMsg extends APIMessage { @APIParam(resourceType = HostVO.class, nonempty = true) private Set hostUuids; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/APIUpdateKVMHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/APIUpdateKVMHostMsg.java index 27f33df1597..54050f2e2f7 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/APIUpdateKVMHostMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/APIUpdateKVMHostMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.log.NoLogging; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -16,6 +17,7 @@ responseClass = APIUpdateHostEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateKVMHostMsg extends APIUpdateHostMsg { @APIParam(maxLength = 255, required = false, emptyString = false) private String username; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryHostOsCategoryMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryHostOsCategoryMsg.java index 4a255faae38..7b7b00d593a 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryHostOsCategoryMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryHostOsCategoryMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryHostOsCategoryReply.class, inventoryClass = HostOsCategoryInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryHostOsCategoryReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryHostOsCategoryMsg extends APIQueryMessage { public static List __example__() { return asList("architecture=x86_64", "osReleaseVersion=\"centos core 7.6.1810\""); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryKvmHypervisorInfoMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryKvmHypervisorInfoMsg.java index 341d33691fb..03f519d009d 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryKvmHypervisorInfoMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/message/APIQueryKvmHypervisorInfoMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by Wenhao.Zhang on 23/02/23 @@ -19,6 +20,7 @@ responseClass = APIQueryKvmHypervisorInfoReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryKvmHypervisorInfoMsg extends APIQueryMessage { public static List __example__() { return asList("uuid=" + uuid()); diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIAddLdapServerMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIAddLdapServerMsg.java index a9b7c2958a4..a433487eb64 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIAddLdapServerMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIAddLdapServerMsg.java @@ -13,6 +13,7 @@ import org.zstack.ldap.entity.LdapEncryptionType; import org.zstack.ldap.entity.LdapServerType; import org.zstack.ldap.entity.LdapServerVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ldap/servers", @@ -20,6 +21,7 @@ responseClass = APIAddLdapServerEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddLdapServerMsg extends APIMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APICreateLdapBindingMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APICreateLdapBindingMsg.java index f75cb6fe8b7..0db4ed54254 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APICreateLdapBindingMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APICreateLdapBindingMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.ldap.entity.LdapServerVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ldap/bindings", @@ -13,6 +14,7 @@ parameterName = "params", responseClass = APICreateLdapBindingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateLdapBindingMsg extends APIMessage { @APIParam(maxLength = 255) private String ldapUid; diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapBindingMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapBindingMsg.java index e4584014dd7..461b7821852 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapBindingMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapBindingMsg.java @@ -6,12 +6,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ldap/bindings/{accountUuid}", method = HttpMethod.DELETE, responseClass = APIDeleteLdapBindingEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLdapBindingMsg extends APIMessage { @APIParam(resourceType = AccountVO.class) private String accountUuid; diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapServerMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapServerMsg.java index 7df708dcd7b..384f2ca70dc 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapServerMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIDeleteLdapServerMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteLdapServerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLdapServerMsg extends APIDeleteMessage { @APIParam(resourceType = LdapServerVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetCandidateLdapEntryForBindingMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetCandidateLdapEntryForBindingMsg.java index 4ecb8424b5f..1c3ef26fd53 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetCandidateLdapEntryForBindingMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetCandidateLdapEntryForBindingMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.ldap.entity.LdapServerVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by lining on 2017/12/03. @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetLdapEntryReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateLdapEntryForBindingMsg extends APISyncCallMessage { @APIParam diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetLdapEntryMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetLdapEntryMsg.java index b63b0ebd0d1..1dd8b8659ac 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetLdapEntryMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIGetLdapEntryMsg.java @@ -7,6 +7,7 @@ import org.zstack.ldap.entity.LdapServerVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by lining on 2017/11/03. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIGetLdapEntryReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetLdapEntryMsg extends APISyncCallMessage { @APIParam diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIQueryLdapServerMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIQueryLdapServerMsg.java index 14a2420bd4f..ddad92393a8 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIQueryLdapServerMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIQueryLdapServerMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryLdapServerReply.class, inventoryClass = LdapServerInventory.class) @RestRequest( @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryLdapServerReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLdapServerMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APISyncAccountsFromLdapServerMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APISyncAccountsFromLdapServerMsg.java index cacfa93e8f0..f115714037b 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APISyncAccountsFromLdapServerMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APISyncAccountsFromLdapServerMsg.java @@ -7,6 +7,7 @@ import org.zstack.identity.imports.entity.SyncCreatedAccountStrategy; import org.zstack.identity.imports.entity.SyncDeletedAccountStrategy; import org.zstack.ldap.entity.LdapServerVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by Wenhao.Zhang on 2024/06/04 @@ -17,6 +18,7 @@ isAction = true, responseClass = APISyncAccountsFromLdapServerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISyncAccountsFromLdapServerMsg extends APIMessage { @APIParam(resourceType = LdapServerVO.class) private String uuid; diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIUpdateLdapServerMsg.java b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIUpdateLdapServerMsg.java index bd49f0ff93b..cf2aeab291f 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/api/APIUpdateLdapServerMsg.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/api/APIUpdateLdapServerMsg.java @@ -10,6 +10,7 @@ import org.zstack.ldap.entity.LdapEncryptionType; import org.zstack.ldap.entity.LdapServerType; import org.zstack.ldap.entity.LdapServerVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ldap/servers/{ldapServerUuid}", @@ -17,6 +18,7 @@ responseClass = APIUpdateLdapServerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateLdapServerMsg extends APIMessage { @APIParam(maxLength = 32, resourceType = LdapServerVO.class) private String ldapServerUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddAccessControlListToLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddAccessControlListToLoadBalancerMsg.java index f2394de1ac2..dcfa08216da 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddAccessControlListToLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddAccessControlListToLoadBalancerMsg.java @@ -1,94 +1,96 @@ -package org.zstack.network.service.lb; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APIEvent; -import org.zstack.header.message.APIMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.other.APIAuditor; -import org.zstack.header.rest.APINoSee; -import org.zstack.header.rest.RestRequest; -import org.zstack.header.acl.AccessControlListVO; - -import java.util.Arrays; -import java.util.List; - -/** - * @author: zhanyong.miao - * @date: 2020-03-11 - **/ -@RestRequest( - path = "/load-balancers/listeners/{listenerUuid}/access-control-lists", - method = HttpMethod.POST, - parameterName = "params", - responseClass = APIAddAccessControlListToLoadBalancerEvent.class -) -public class APIAddAccessControlListToLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { - @APIParam(resourceType = AccessControlListVO.class, nonempty = true) - private List aclUuids; - @APIParam(validValues = {"white","black","redirect"}) - private String aclType; - @APIParam(resourceType = LoadBalancerListenerVO.class) - private String listenerUuid; - @APIParam(resourceType = LoadBalancerServerGroupVO.class, required = false) - private List serverGroupUuids; - @APINoSee - private String loadBalancerUuid; - - - public List getAclUuids() { - return aclUuids; - } - - public void setAclUuids(List aclUuids) { - this.aclUuids = aclUuids; - } - - public String getAclType() { - return aclType; - } - - public void setAclType(String aclType) { - this.aclType = aclType; - } - - public String getListenerUuid() { - return listenerUuid; - } - - public void setListenerUuid(String listenerUuid) { - this.listenerUuid = listenerUuid; - } - - public List getServerGroupUuids() { - return serverGroupUuids; - } - - public void setServerGroupUuids(List serverGroupUuids) { - this.serverGroupUuids = serverGroupUuids; - } - - @Override - public String getLoadBalancerUuid() { - return loadBalancerUuid; - } - - public void setLoadBalancerUuid(String loadBalancerUuid) { - this.loadBalancerUuid = loadBalancerUuid; - } - - public static APIAddAccessControlListToLoadBalancerMsg __example__() { - APIAddAccessControlListToLoadBalancerMsg msg = new APIAddAccessControlListToLoadBalancerMsg(); - - msg.setAclUuids(Arrays.asList(uuid())); - msg.setListenerUuid(uuid()); - msg.setLoadBalancerUuid(uuid()); - msg.setAclType(LoadBalancerAclType.black.toString()); - - return msg; - } - - @Override - public APIAuditor.Result audit(APIMessage msg, APIEvent rsp) { - return new Result(((APIAddAccessControlListToLoadBalancerMsg)msg).getLoadBalancerUuid(), LoadBalancerVO.class); - } +package org.zstack.network.service.lb; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.APINoSee; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.acl.AccessControlListVO; + +import java.util.Arrays; +import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-11 + **/ +@RestRequest( + path = "/load-balancers/listeners/{listenerUuid}/access-control-lists", + method = HttpMethod.POST, + parameterName = "params", + responseClass = APIAddAccessControlListToLoadBalancerEvent.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIAddAccessControlListToLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { + @APIParam(resourceType = AccessControlListVO.class, nonempty = true) + private List aclUuids; + @APIParam(validValues = {"white","black","redirect"}) + private String aclType; + @APIParam(resourceType = LoadBalancerListenerVO.class) + private String listenerUuid; + @APIParam(resourceType = LoadBalancerServerGroupVO.class, required = false) + private List serverGroupUuids; + @APINoSee + private String loadBalancerUuid; + + + public List getAclUuids() { + return aclUuids; + } + + public void setAclUuids(List aclUuids) { + this.aclUuids = aclUuids; + } + + public String getAclType() { + return aclType; + } + + public void setAclType(String aclType) { + this.aclType = aclType; + } + + public String getListenerUuid() { + return listenerUuid; + } + + public void setListenerUuid(String listenerUuid) { + this.listenerUuid = listenerUuid; + } + + public List getServerGroupUuids() { + return serverGroupUuids; + } + + public void setServerGroupUuids(List serverGroupUuids) { + this.serverGroupUuids = serverGroupUuids; + } + + @Override + public String getLoadBalancerUuid() { + return loadBalancerUuid; + } + + public void setLoadBalancerUuid(String loadBalancerUuid) { + this.loadBalancerUuid = loadBalancerUuid; + } + + public static APIAddAccessControlListToLoadBalancerMsg __example__() { + APIAddAccessControlListToLoadBalancerMsg msg = new APIAddAccessControlListToLoadBalancerMsg(); + + msg.setAclUuids(Arrays.asList(uuid())); + msg.setListenerUuid(uuid()); + msg.setLoadBalancerUuid(uuid()); + msg.setAclType(LoadBalancerAclType.black.toString()); + + return msg; + } + + @Override + public APIAuditor.Result audit(APIMessage msg, APIEvent rsp) { + return new Result(((APIAddAccessControlListToLoadBalancerMsg)msg).getLoadBalancerUuid(), LoadBalancerVO.class); + } } \ No newline at end of file diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddBackendServerToServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddBackendServerToServerGroupMsg.java index 83242bc3ddd..0de5248d046 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddBackendServerToServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddBackendServerToServerGroupMsg.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/servergroups/{serverGroupUuid}/backendservers", @@ -19,6 +20,7 @@ parameterName = "params", responseClass = APIAddBackendServerToServerGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddBackendServerToServerGroupMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddCertificateToLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddCertificateToLoadBalancerListenerMsg.java index aac6993595b..6e57c0ff4c3 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddCertificateToLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddCertificateToLoadBalancerListenerMsg.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.MetadataImpact; /** * Created by shixin on 03/26/2018. @@ -17,6 +18,7 @@ parameterName = "params", responseClass = APIAddCertificateToLoadBalancerListenerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddCertificateToLoadBalancerListenerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = CertificateVO.class, nonempty = true) private String certificateUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddServerGroupToLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddServerGroupToLoadBalancerListenerMsg.java index 7fddac65a13..47fb397b922 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddServerGroupToLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddServerGroupToLoadBalancerListenerMsg.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.MetadataImpact; @RestRequest( path = "/load-balancers/listeners/{listenerUuid}/servergroups", @@ -14,6 +15,7 @@ parameterName = "params", responseClass = APIAddServerGroupToLoadBalancerListenerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddServerGroupToLoadBalancerListenerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddVmNicToLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddVmNicToLoadBalancerMsg.java index 542cd189b96..fd171fbbb19 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddVmNicToLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIAddVmNicToLoadBalancerMsg.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -21,6 +22,7 @@ parameterName = "params", responseClass = APIAddVmNicToLoadBalancerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddVmNicToLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = VmNicVO.class, nonempty = true) private List vmNicUuids; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeAccessControlListServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeAccessControlListServerGroupMsg.java index 89a331b1b43..baafa9be997 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeAccessControlListServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeAccessControlListServerGroupMsg.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/listener/acl/{aclUuid}/servergroup/actions", @@ -18,6 +19,7 @@ responseClass = APIChangeAccessControlListServerGroupEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeAccessControlListServerGroupMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private List serverGroupUuids; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerBackendServerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerBackendServerMsg.java index 2a65370f874..6fb5efc1a95 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerBackendServerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerBackendServerMsg.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/servergroups/{serverGroupUuid}/backendserver/actions", @@ -19,6 +20,7 @@ responseClass = APIChangeLoadBalancerBackendServerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeLoadBalancerBackendServerMsg extends APIMessage implements LoadBalancerMessage , APIAuditor{ @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java index 29510850bc8..246511e90e3 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIChangeLoadBalancerListenerMsg.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.MetadataImpact; /** * Created by shixin.ruan on 02/25/2019. @@ -15,6 +16,7 @@ responseClass = APIChangeLoadBalancerListenerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeLoadBalancerListenerMsg extends APIMessage implements LoadBalancerListenerMsg , LoadBalancerMessage { @APIParam(resourceType = LoadBalancerListenerVO.class) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateCertificateMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateCertificateMsg.java index 24d1b19bb26..c882b81d3c1 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateCertificateMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateCertificateMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin on 03/22/2018. @@ -19,6 +20,7 @@ responseClass = APICreateCertificateEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateCertificateMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java index aa1be52f2bf..9cd3c6b1035 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerListenerMsg.java @@ -11,6 +11,7 @@ import org.zstack.header.acl.AccessControlListVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -22,6 +23,7 @@ responseClass = APICreateLoadBalancerListenerEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateLoadBalancerListenerMsg extends APICreateMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerVO.class) private String loadBalancerUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerMsg.java index bb6d5a40dc9..f6c906e90d8 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; import org.zstack.network.service.vip.VipVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -20,6 +21,7 @@ responseClass = APICreateLoadBalancerEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateLoadBalancerMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerServerGroupMsg.java index 7edfa76d81c..eb942b35431 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APICreateLoadBalancerServerGroupMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(LoadBalancerVO.class) @RestRequest( @@ -17,6 +18,8 @@ parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APICreateLoadBalancerServerGroupMsg extends APICreateMessage implements LoadBalancerMessage, APIAuditor { @APIParam(maxLength = 255) private String name; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteCertificateMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteCertificateMsg.java index 169007b239a..e1faececcf9 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteCertificateMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteCertificateMsg.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.MetadataImpact; /** * Created by shixin on 03/22/2015. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteCertificateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteCertificateMsg extends APIDeleteMessage { @APIParam(resourceType = CertificateVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerListenerMsg.java index b849a1ab437..b71d57c1476 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerListenerMsg.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.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteLoadBalancerListenerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLoadBalancerListenerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APINoSee private String loadBalancerUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerMsg.java index 320550f1e73..70c9e52ad5a 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerMsg.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.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteLoadBalancerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLoadBalancerMsg extends APIDeleteMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerServerGroupMsg.java index 96c6e5f3c2c..e0260187938 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIDeleteLoadBalancerServerGroupMsg.java @@ -7,12 +7,14 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/servergroups/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteLoadBalancerServerGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteLoadBalancerServerGroupMsg extends APIMessage implements LoadBalancerMessage, APIAuditor{ @APIParam(resourceType = LoadBalancerServerGroupVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForLoadBalancerMsg.java index 622cf935176..bdf3cfe34a0 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForLoadBalancerMsg.java @@ -1,49 +1,51 @@ -package org.zstack.network.service.lb; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APIGetMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.rest.APINoSee; -import org.zstack.header.rest.RestRequest; - -/** - * @author: zhanyong.miao - * @date: 2020-04-15 - **/ -@RestRequest( - path = "/load-balancers/listeners/{listenerUuid}/networks/candidates", - method = HttpMethod.GET, - responseClass = APIGetCandidateL3NetworksForLoadBalancerReply.class -) -public class APIGetCandidateL3NetworksForLoadBalancerMsg extends APIGetMessage implements LoadBalancerMessage { - @APIParam(resourceType = LoadBalancerListenerVO.class) - private String listenerUuid; - @APINoSee - private String loadBalancerUuid; - - public String getListenerUuid() { - return listenerUuid; - } - - public void setListenerUuid(String listenerUuid) { - this.listenerUuid = listenerUuid; - } - - public String getLoadBalancerUuid() { - return loadBalancerUuid; - } - - public void setLoadBalancerUuid(String loadBalancerUuid) { - this.loadBalancerUuid = loadBalancerUuid; - } - - - public static APIGetCandidateL3NetworksForLoadBalancerMsg __example__() { - APIGetCandidateL3NetworksForLoadBalancerMsg msg = new APIGetCandidateL3NetworksForLoadBalancerMsg(); - - msg.setListenerUuid(uuid()); - msg.setLoadBalancerUuid(uuid()); - - return msg; - } -} +package org.zstack.network.service.lb; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIGetMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.APINoSee; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-04-15 + **/ +@RestRequest( + path = "/load-balancers/listeners/{listenerUuid}/networks/candidates", + method = HttpMethod.GET, + responseClass = APIGetCandidateL3NetworksForLoadBalancerReply.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIGetCandidateL3NetworksForLoadBalancerMsg extends APIGetMessage implements LoadBalancerMessage { + @APIParam(resourceType = LoadBalancerListenerVO.class) + private String listenerUuid; + @APINoSee + private String loadBalancerUuid; + + public String getListenerUuid() { + return listenerUuid; + } + + public void setListenerUuid(String listenerUuid) { + this.listenerUuid = listenerUuid; + } + + public String getLoadBalancerUuid() { + return loadBalancerUuid; + } + + public void setLoadBalancerUuid(String loadBalancerUuid) { + this.loadBalancerUuid = loadBalancerUuid; + } + + + public static APIGetCandidateL3NetworksForLoadBalancerMsg __example__() { + APIGetCandidateL3NetworksForLoadBalancerMsg msg = new APIGetCandidateL3NetworksForLoadBalancerMsg(); + + msg.setListenerUuid(uuid()); + msg.setLoadBalancerUuid(uuid()); + + return msg; + } +} diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForServerGroupMsg.java index 0fa9672f454..fbb52384ee8 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateL3NetworksForServerGroupMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIGetMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @author: sulin.sheng @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateL3NetworksForServerGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateL3NetworksForServerGroupMsg extends APIGetMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerServerGroupVO.class, required = false) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerMsg.java index 476934c2c7a..9f821d3e782 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/11/29. @@ -14,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateVmNicsForLoadBalancerReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateVmNicsForLoadBalancerMsg extends APISyncCallMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerListenerVO.class) private String listenerUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerServerGroupMsg.java index f1ae880e7cf..76756a13e6c 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetCandidateVmNicsForLoadBalancerServerGroupMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin.ruan on 2020/11/09. @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateVmNicsForLoadBalancerServerGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateVmNicsForLoadBalancerServerGroupMsg extends APISyncCallMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerServerGroupVO.class, required = false) private String servergroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetLoadBalancerListenerACLEntriesMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetLoadBalancerListenerACLEntriesMsg.java index 6d3676ce75e..6a235fe412c 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetLoadBalancerListenerACLEntriesMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIGetLoadBalancerListenerACLEntriesMsg.java @@ -7,12 +7,14 @@ import org.zstack.header.rest.RestRequest; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/listeners/access-control-lists/entries", method = HttpMethod.GET, responseClass = APIGetLoadBalancerListenerACLEntriesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetLoadBalancerListenerACLEntriesMsg extends APISyncCallMessage { @APIParam(resourceType = LoadBalancerListenerVO.class, required = false) private List listenerUuids; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryCertificateMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryCertificateMsg.java index b905adac0a4..a21ad5dc96b 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryCertificateMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryCertificateMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin on 03/22/2018. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryCertificateReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryCertificateMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerListenerMsg.java index eedd2bb71ed..59d0eebb766 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerListenerMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/18/2015. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryLoadBalancerListenerReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLoadBalancerListenerMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerMsg.java index 049ad614fd9..7a0a4880024 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/18/2015. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryLoadBalancerReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLoadBalancerMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerServerGroupMsg.java index c3d960e497c..997f9ca1585 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIQueryLoadBalancerServerGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryLoadBalancerServerGroupReply.class, inventoryClass = LoadBalancerServerGroupInventory.class) @RestRequest( path = "/load-balancers/servergroups", @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryLoadBalancerServerGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLoadBalancerServerGroupMsg extends APIQueryMessage{ public static List __example__() { return asList(); diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRefreshLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRefreshLoadBalancerMsg.java index 80fa49835cf..4761eae0b8b 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRefreshLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRefreshLoadBalancerMsg.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.MetadataImpact; /** * Created by frank on 8/18/2015. @@ -14,6 +15,7 @@ responseClass = APIRefreshLoadBalancerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRefreshLoadBalancerMsg extends APIMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerVO.class) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveAccessControlListFromLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveAccessControlListFromLoadBalancerMsg.java index b10c52920f8..e41662b9c76 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveAccessControlListFromLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveAccessControlListFromLoadBalancerMsg.java @@ -1,81 +1,83 @@ -package org.zstack.network.service.lb; - -import org.springframework.http.HttpMethod; -import org.zstack.header.message.APIEvent; -import org.zstack.header.message.APIMessage; -import org.zstack.header.message.APIParam; -import org.zstack.header.other.APIAuditor; -import org.zstack.header.rest.APINoSee; -import org.zstack.header.rest.RestRequest; -import org.zstack.header.acl.AccessControlListVO; - -import java.util.Arrays; -import java.util.List; - -/** - * @author: zhanyong.miao - * @date: 2020-03-11 - **/ -@RestRequest( - path = "/load-balancers/listeners/{listenerUuid}/access-control-lists", - method = HttpMethod.DELETE, - responseClass = APIRemoveAccessControlListFromLoadBalancerEvent.class -) -public class APIRemoveAccessControlListFromLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { - @APIParam(resourceType = AccessControlListVO.class, nonempty = true) - private List aclUuids; - @APINoSee - private String loadBalancerUuid; - @APIParam(resourceType = LoadBalancerListenerVO.class) - private String listenerUuid; - - @APIParam(required = false, resourceType = LoadBalancerServerGroupVO.class) - private List serverGroupUuids; - - public List getAclUuids() { - return aclUuids; - } - - public void setAclUuids(List aclUuids) { - this.aclUuids = aclUuids; - } - - public String getListenerUuid() { - return listenerUuid; - } - - public void setListenerUuid(String listenerUuid) { - this.listenerUuid = listenerUuid; - } - - public List getServerGroupUuids() { - return serverGroupUuids; - } - - public void setServerGroupUuids(List serverGroupUuids) { - this.serverGroupUuids = serverGroupUuids; - } - - @Override - public String getLoadBalancerUuid() { - return loadBalancerUuid; - } - - public void setLoadBalancerUuid(String loadBalancerUuid) { - this.loadBalancerUuid = loadBalancerUuid; - } - - public static APIRemoveAccessControlListFromLoadBalancerMsg __example__() { - APIRemoveAccessControlListFromLoadBalancerMsg msg = new APIRemoveAccessControlListFromLoadBalancerMsg(); - - msg.setListenerUuid(uuid()); - msg.setAclUuids(Arrays.asList(uuid())); - - return msg; - } - - @Override - public Result audit(APIMessage msg, APIEvent rsp) { - return new Result(((APIRemoveAccessControlListFromLoadBalancerMsg)msg).loadBalancerUuid, LoadBalancerVO.class); - } -} +package org.zstack.network.service.lb; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.APINoSee; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.acl.AccessControlListVO; + +import java.util.Arrays; +import java.util.List; +import org.zstack.header.vm.MetadataImpact; + +/** + * @author: zhanyong.miao + * @date: 2020-03-11 + **/ +@RestRequest( + path = "/load-balancers/listeners/{listenerUuid}/access-control-lists", + method = HttpMethod.DELETE, + responseClass = APIRemoveAccessControlListFromLoadBalancerEvent.class +) +@MetadataImpact(MetadataImpact.Impact.NONE) +public class APIRemoveAccessControlListFromLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { + @APIParam(resourceType = AccessControlListVO.class, nonempty = true) + private List aclUuids; + @APINoSee + private String loadBalancerUuid; + @APIParam(resourceType = LoadBalancerListenerVO.class) + private String listenerUuid; + + @APIParam(required = false, resourceType = LoadBalancerServerGroupVO.class) + private List serverGroupUuids; + + public List getAclUuids() { + return aclUuids; + } + + public void setAclUuids(List aclUuids) { + this.aclUuids = aclUuids; + } + + public String getListenerUuid() { + return listenerUuid; + } + + public void setListenerUuid(String listenerUuid) { + this.listenerUuid = listenerUuid; + } + + public List getServerGroupUuids() { + return serverGroupUuids; + } + + public void setServerGroupUuids(List serverGroupUuids) { + this.serverGroupUuids = serverGroupUuids; + } + + @Override + public String getLoadBalancerUuid() { + return loadBalancerUuid; + } + + public void setLoadBalancerUuid(String loadBalancerUuid) { + this.loadBalancerUuid = loadBalancerUuid; + } + + public static APIRemoveAccessControlListFromLoadBalancerMsg __example__() { + APIRemoveAccessControlListFromLoadBalancerMsg msg = new APIRemoveAccessControlListFromLoadBalancerMsg(); + + msg.setListenerUuid(uuid()); + msg.setAclUuids(Arrays.asList(uuid())); + + return msg; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new Result(((APIRemoveAccessControlListFromLoadBalancerMsg)msg).loadBalancerUuid, LoadBalancerVO.class); + } +} diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveBackendServerFromServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveBackendServerFromServerGroupMsg.java index 0ad3466827f..28bdc5eb8b0 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveBackendServerFromServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveBackendServerFromServerGroupMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.vm.VmNicVO; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/servergroups/{serverGroupUuid}/backendservers/actions", @@ -17,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APIRemoveBackendServerFromServerGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveBackendServerFromServerGroupMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveCertificateFromLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveCertificateFromLoadBalancerListenerMsg.java index 9d8af9b2f7d..9243eead4d9 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveCertificateFromLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveCertificateFromLoadBalancerListenerMsg.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.MetadataImpact; /** * Created by shixin on 03/26/2018. @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveCertificateFromLoadBalancerListenerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveCertificateFromLoadBalancerListenerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = CertificateVO.class, nonempty = true) private String certificateUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveServerGroupFromLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveServerGroupFromLoadBalancerListenerMsg.java index 3aeb6db4385..74ea5ca5b3a 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveServerGroupFromLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveServerGroupFromLoadBalancerListenerMsg.java @@ -7,12 +7,14 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/load-balancers/listeners/{listenerUuid}/servergroups", method = HttpMethod.DELETE, responseClass = APIRemoveServerGroupFromLoadBalancerListenerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveServerGroupFromLoadBalancerListenerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = LoadBalancerServerGroupVO.class, nonempty = true) private String serverGroupUuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveVmNicFromLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveVmNicFromLoadBalancerMsg.java index 847b408399b..e960f984e9f 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveVmNicFromLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIRemoveVmNicFromLoadBalancerMsg.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 8/8/2015. @@ -20,6 +21,7 @@ method = HttpMethod.DELETE, responseClass = APIRemoveVmNicFromLoadBalancerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveVmNicFromLoadBalancerMsg extends APIMessage implements LoadBalancerMessage, APIAuditor { @APIParam(resourceType = VmNicVO.class, nonempty = true) private List vmNicUuids; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateCertificateMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateCertificateMsg.java index 42712422029..778634620f5 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateCertificateMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateCertificateMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APICreateMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin on 04/12/2018. @@ -14,6 +15,7 @@ responseClass = APIUpdateCertificateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateCertificateMsg extends APICreateMessage { @APIParam(resourceType = CertificateVO.class) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerListenerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerListenerMsg.java index a1917f657e5..edf2b9cfc89 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerListenerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerListenerMsg.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.MetadataImpact; /** * Created by camile on 5/19/2017. @@ -15,6 +16,7 @@ responseClass = APIUpdateLoadBalancerListenerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateLoadBalancerListenerMsg extends APIMessage implements LoadBalancerListenerMsg , LoadBalancerMessage { @APIParam(resourceType = LoadBalancerListenerVO.class) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerMsg.java index 94ee6e2e71f..03863d73768 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APICreateMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by camile on 5/18/2017. @@ -15,6 +16,7 @@ isAction = true //parameterName = "updateLoadBalancer" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateLoadBalancerMsg extends APICreateMessage implements LoadBalancerMessage { @APIParam(resourceType = LoadBalancerVO.class) private String uuid; diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerServerGroupMsg.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerServerGroupMsg.java index 5ab0e58229a..e20bd7e386b 100644 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerServerGroupMsg.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/APIUpdateLoadBalancerServerGroupMsg.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.MetadataImpact; @RestRequest( path = "/load-balancers/servergroups/{uuid}/actions", @@ -12,6 +13,7 @@ responseClass = APIUpdateLoadBalancerServerGroupEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateLoadBalancerServerGroupMsg extends APIMessage implements LoadBalancerMessage{ @APIParam(resourceType = LoadBalancerServerGroupVO.class) private String uuid; diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIAddLocalPrimaryStorageMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIAddLocalPrimaryStorageMsg.java index e5b9522fe3f..e4d049fba1b 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIAddLocalPrimaryStorageMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIAddLocalPrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.storage.primary.APIAddPrimaryStorageMsg; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/1/2015. @@ -17,6 +18,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddLocalPrimaryStorageMsg extends APIAddPrimaryStorageMsg { @Override public String getType() { diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIGetLocalStorageHostDiskCapacityMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIGetLocalStorageHostDiskCapacityMsg.java index 385a0e22647..ac1d7ac8717 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIGetLocalStorageHostDiskCapacityMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIGetLocalStorageHostDiskCapacityMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 10/15/2015. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIGetLocalStorageHostDiskCapacityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetLocalStorageHostDiskCapacityMsg extends APISyncCallMessage implements PrimaryStorageMessage { @APIParam(resourceType = HostVO.class, required = false) private String hostUuid; diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageGetVolumeMigratableHostsMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageGetVolumeMigratableHostsMsg.java index 3296e618549..af3fea8d5a5 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageGetVolumeMigratableHostsMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageGetVolumeMigratableHostsMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.volume.VolumeVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/18/2015. @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APILocalStorageGetVolumeMigratableReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APILocalStorageGetVolumeMigratableHostsMsg extends APISyncCallMessage implements PrimaryStorageMessage { @APIParam(resourceType = VolumeVO.class) private String volumeUuid; diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIQueryLocalStorageResourceRefMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIQueryLocalStorageResourceRefMsg.java index bbe30a2d6c5..b88f33dd08e 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIQueryLocalStorageResourceRefMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APIQueryLocalStorageResourceRefMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 11/14/2015. @@ -17,6 +18,7 @@ method = HttpMethod.GET, responseClass = APIQueryLocalStorageResourceRefReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryLocalStorageResourceRefMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/APIAddNfsPrimaryStorageMsg.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/APIAddNfsPrimaryStorageMsg.java index 2352874c4ab..ef7f549635a 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/APIAddNfsPrimaryStorageMsg.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/APIAddNfsPrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.storage.primary.APIAddPrimaryStorageMsg; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -59,6 +60,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddNfsPrimaryStorageMsg extends APIAddPrimaryStorageMsg { public APIAddNfsPrimaryStorageMsg() { this.setType(NfsPrimaryStorageConstant.NFS_PRIMARY_STORAGE_TYPE); diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIAttachPortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIAttachPortForwardingRuleMsg.java index 7e99555195d..6503f9bcb5d 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIAttachPortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIAttachPortForwardingRuleMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -50,6 +51,7 @@ parameterName = "params", responseClass = APIAttachPortForwardingRuleEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachPortForwardingRuleMsg extends APIMessage { /** * @desc rule uuid diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIChangePortForwardingRuleStateMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIChangePortForwardingRuleStateMsg.java index 25b577527f8..f8be5b107a5 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIChangePortForwardingRuleStateMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIChangePortForwardingRuleStateMsg.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.MetadataImpact; /** */ @@ -13,6 +14,7 @@ isAction = true, responseClass = APIChangePortForwardingRuleStateEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangePortForwardingRuleStateMsg extends APIMessage { @APIParam(resourceType = PortForwardingRuleVO.class) private String uuid; diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APICreatePortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APICreatePortForwardingRuleMsg.java index 856e7295185..6a2e853e5fc 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APICreatePortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APICreatePortForwardingRuleMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; import org.zstack.network.service.vip.VipVO; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -68,6 +69,7 @@ responseClass = APICreatePortForwardingRuleEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreatePortForwardingRuleMsg extends APICreateMessage implements APIAuditor { /** * @desc uuid of vip the rule is being created on diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDeletePortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDeletePortForwardingRuleMsg.java index 891b7313811..404f37d50d6 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDeletePortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDeletePortForwardingRuleMsg.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.MetadataImpact; /** * @api @@ -50,6 +51,7 @@ method = HttpMethod.DELETE, responseClass = APIDeletePortForwardingRuleEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeletePortForwardingRuleMsg extends APIDeleteMessage { /** * @desc diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDetachPortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDetachPortForwardingRuleMsg.java index 1fd298ff62b..0b904417134 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDetachPortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIDetachPortForwardingRuleMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -49,6 +50,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachPortForwardingRuleEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachPortForwardingRuleMsg extends APIMessage implements APIAuditor { /** * @desc rule uuid diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIGetPortForwardingAttachableVmNicsMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIGetPortForwardingAttachableVmNicsMsg.java index f3bc6dfd9f5..931c3ef960e 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIGetPortForwardingAttachableVmNicsMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIGetPortForwardingAttachableVmNicsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetPortForwardingAttachableVmNicsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetPortForwardingAttachableVmNicsMsg extends APISyncCallMessage { @APIParam(resourceType = PortForwardingRuleVO.class) private String ruleUuid; diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIQueryPortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIQueryPortForwardingRuleMsg.java index eac0dd79af2..1705dc89166 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIQueryPortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIQueryPortForwardingRuleMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryPortForwardingRuleReply.class, inventoryClass = PortForwardingRuleInventory.class) @RestRequest( @@ -16,6 +17,7 @@ optionalPaths = {"/port-forwarding/{uuid}"}, responseClass = APIQueryPortForwardingRuleReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryPortForwardingRuleMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIUpdatePortForwardingRuleMsg.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIUpdatePortForwardingRuleMsg.java index 8d874a1fd70..4e83e288845 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIUpdatePortForwardingRuleMsg.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/APIUpdatePortForwardingRuleMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdatePortForwardingRuleEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdatePortForwardingRuleMsg extends APIMessage { @APIParam(resourceType = PortForwardingRuleVO.class) private String uuid; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIAddSdnControllerMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIAddSdnControllerMsg.java index 59bd437b013..465ef691cb7 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIAddSdnControllerMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIAddSdnControllerMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(SdnControllerVO.class) @RestRequest( @@ -14,6 +15,7 @@ responseClass = APIAddSdnControllerEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSdnControllerMsg extends APICreateMessage implements APIAuditor { @APIParam(maxLength = 255) private String vendorType; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkMsg.java index 054738a6712..45e67fbf931 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.network.l2.APICreateL2NetworkMsg; import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; @OverriddenApiParams({ @OverriddenApiParam(field = "physicalInterface", param = @APIParam(maxLength = 1024, required = false)), @@ -18,6 +19,7 @@ responseClass = APICreateL2HardwareVxlanNetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2HardwareVxlanNetworkMsg extends APICreateL2NetworkMsg { @APIParam(required = false, numberRange = {1, 16777214}) private Integer vni; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkPoolMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkPoolMsg.java index efccb43fa72..56a501dbe57 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkPoolMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APICreateL2HardwareVxlanNetworkPoolMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.network.l2.APICreateL2NetworkMsg; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/l2-networks/hardware-vxlan-pool", @@ -11,6 +12,7 @@ responseClass = APICreateL2HardwareVxlanNetworkPoolEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2HardwareVxlanNetworkPoolMsg extends APICreateL2NetworkMsg { @APIParam(resourceType = SdnControllerVO.class) private String sdnControllerUuid; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIQuerySdnControllerMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIQuerySdnControllerMsg.java index 62225c5b0bf..8a28fed8afd 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIQuerySdnControllerMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIQuerySdnControllerMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -22,6 +23,7 @@ method = HttpMethod.GET, responseClass = APIQuerySdnControllerReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySdnControllerMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIRemoveSdnControllerMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIRemoveSdnControllerMsg.java index 7a6aef5419c..0f705061452 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIRemoveSdnControllerMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIRemoveSdnControllerMsg.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.MetadataImpact; @RestRequest( path = "/sdn-controllers/{uuid}", method = HttpMethod.DELETE, responseClass = APIRemoveSdnControllerEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRemoveSdnControllerMsg extends APIDeleteMessage { @APIParam(successIfResourceNotExisting = true, resourceType = SdnControllerVO.class) private String uuid; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIUpdateSdnControllerMsg.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIUpdateSdnControllerMsg.java index 06a24c57457..2e53879b161 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIUpdateSdnControllerMsg.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/header/APIUpdateSdnControllerMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.network.l2.L2NetworkMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin.ruan on 09/19/2019. @@ -15,6 +16,7 @@ responseClass = APIUpdateSdnControllerEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateSdnControllerMsg extends APIMessage implements L2NetworkMessage { @APIParam(resourceType = SdnControllerVO.class) private String uuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddSecurityGroupRuleMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddSecurityGroupRuleMsg.java index 0f2a95b67cf..a904f1d5d28 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddSecurityGroupRuleMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddSecurityGroupRuleMsg.java @@ -10,6 +10,7 @@ import java.util.Objects; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -102,6 +103,7 @@ responseClass = APIAddSecurityGroupRuleEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSecurityGroupRuleMsg extends APIMessage implements AddSecurityGroupRuleMessage { /** * @inventory diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddVmNicToSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddVmNicToSecurityGroupMsg.java index 147a093852f..2d486a08087 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddVmNicToSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAddVmNicToSecurityGroupMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -59,6 +60,7 @@ responseClass = APIAddVmNicToSecurityGroupEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddVmNicToSecurityGroupMsg extends APIMessage { /** * @desc security group uuid diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAttachSecurityGroupToL3NetworkMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAttachSecurityGroupToL3NetworkMsg.java index e4322e8c969..ef4889c24ba 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAttachSecurityGroupToL3NetworkMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAttachSecurityGroupToL3NetworkMsg.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.MetadataImpact; /** * @api @@ -50,6 +51,7 @@ parameterName = "params", responseClass = APIAttachSecurityGroupToL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachSecurityGroupToL3NetworkMsg extends APIMessage implements SecurityGroupMessage { @APIParam(resourceType=SecurityGroupVO.class) private String securityGroupUuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleMsg.java index 08c465887a0..d44678fc18c 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleMsg.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.MetadataImpact; @RestRequest( @@ -13,6 +14,8 @@ isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APIChangeSecurityGroupRuleMsg extends APIMessage{ @APIParam(resourceType = SecurityGroupRuleVO.class, nonempty = true) private String uuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleStateMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleStateMsg.java index 9120e35173a..997c24e0053 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleStateMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupRuleStateMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/security-groups/{securityGroupUuid}/rules/state/actions", @@ -15,6 +16,7 @@ responseClass = APIChangeSecurityGroupRuleStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeSecurityGroupRuleStateMsg extends APIMessage implements SecurityGroupMessage { @APIParam(required = true, nonempty = true) private String securityGroupUuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupStateMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupStateMsg.java index a206abeaca5..ac60ceb1ee2 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupStateMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeSecurityGroupStateMsg.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.MetadataImpact; /** * @api @@ -51,6 +52,7 @@ responseClass = APIChangeSecurityGroupStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeSecurityGroupStateMsg extends APIMessage { @APIParam(resourceType = SecurityGroupVO.class) private String uuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeVmNicSecurityPolicyMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeVmNicSecurityPolicyMsg.java index 43192344e1e..46b00c9999d 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeVmNicSecurityPolicyMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIChangeVmNicSecurityPolicyMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmNicVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/security-groups/nics/{vmNicUuid}/security-policy/actions", @@ -12,6 +13,7 @@ responseClass = APIChangeVmNicSecurityPolicyEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeVmNicSecurityPolicyMsg extends APIMessage implements VmNicSecurityGroupMessage { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APICreateSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APICreateSecurityGroupMsg.java index 49c2e99a096..6c8982438db 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APICreateSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APICreateSecurityGroupMsg.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.MetadataImpact; /** * @api @@ -50,6 +51,7 @@ responseClass = APICreateSecurityGroupEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateSecurityGroupMsg extends APICreateMessage implements APIAuditor { /** * @desc max length of 255 characters diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupMsg.java index 76bbd319790..0ba30488ddb 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static org.zstack.utils.CollectionDSL.list; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -52,6 +53,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteSecurityGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteSecurityGroupMsg extends APIDeleteMessage implements SecurityGroupMessage { /** * @desc security group uuid diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupRuleMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupRuleMsg.java index 01bceb920c7..bfbd89da29a 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupRuleMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteSecurityGroupRuleMsg.java @@ -10,6 +10,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -58,6 +59,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteSecurityGroupRuleEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteSecurityGroupRuleMsg extends APIMessage implements APIAuditor { /** * @desc a list of rule uuid diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteVmNicFromSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteVmNicFromSecurityGroupMsg.java index f8c9b5cfbef..1c1c44ef176 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteVmNicFromSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDeleteVmNicFromSecurityGroupMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -55,6 +56,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmNicFromSecurityGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVmNicFromSecurityGroupMsg extends APIMessage { /** * @desc security group uuid diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDetachSecurityGroupFromL3NetworkMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDetachSecurityGroupFromL3NetworkMsg.java index d2555d35f47..b508fb14f6a 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDetachSecurityGroupFromL3NetworkMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIDetachSecurityGroupFromL3NetworkMsg.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.MetadataImpact; /** @@ -42,6 +43,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachSecurityGroupFromL3NetworkEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachSecurityGroupFromL3NetworkMsg extends APIMessage implements SecurityGroupMessage { /** * @desc security group uuid diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIGetCandidateVmNicForSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIGetCandidateVmNicForSecurityGroupMsg.java index e230f92368d..f67039635d9 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIGetCandidateVmNicForSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIGetCandidateVmNicForSecurityGroupMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateVmNicForSecurityGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetCandidateVmNicForSecurityGroupMsg extends APISyncCallMessage { @APIParam(resourceType = SecurityGroupVO.class) private String securityGroupUuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupMsg.java index e8719e627e4..55191e88b3d 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQuerySecurityGroupReply.class, inventoryClass = SecurityGroupInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQuerySecurityGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySecurityGroupMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupRuleMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupRuleMsg.java index c7576ed6475..0e5e8fec8c2 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupRuleMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQuerySecurityGroupRuleMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -18,6 +19,7 @@ method = HttpMethod.GET, responseClass = APIQuerySecurityGroupRuleReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySecurityGroupRuleMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicInSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicInSecurityGroupMsg.java index 8ed12c7e43f..756fc9c73e8 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicInSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicInSecurityGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVmNicInSecurityGroupReply.class, inventoryClass = VmNicSecurityGroupRefInventory.class) @RestRequest( @@ -15,6 +16,7 @@ method = HttpMethod.GET, responseClass = APIQueryVmNicInSecurityGroupReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVmNicInSecurityGroupMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicSecurityPolicyMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicSecurityPolicyMsg.java index adcb6cb810a..77bb277017d 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicSecurityPolicyMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIQueryVmNicSecurityPolicyMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVmNicSecurityPolicyReply.class, inventoryClass = VmNicSecurityPolicyInventory.class) @RestRequest( @@ -17,6 +18,8 @@ responseClass = APIQueryVmNicSecurityPolicyReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APIQueryVmNicSecurityPolicyMsg extends APIQueryMessage { public static List __example__() { return asList("ingressPolicy=DROP", "egressPolicy=DROP"); diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APISetVmNicSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APISetVmNicSecurityGroupMsg.java index 45598831bca..e95611ae2e8 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APISetVmNicSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APISetVmNicSecurityGroupMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/security-groups/nics/{vmNicUuid}/actions", @@ -15,6 +16,7 @@ responseClass = APISetVmNicSecurityGroupEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APISetVmNicSecurityGroupMsg extends APIMessage implements VmNicSecurityGroupMessage { @APIParam(resourceType = VmNicVO.class, nonempty = true, required = true) private String vmNicUuid; diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupMsg.java index bf0eafcc71e..ff6cc322e9c 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ isAction = true, responseClass = APIUpdateSecurityGroupEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateSecurityGroupMsg extends APIMessage implements SecurityGroupMessage { @APIParam(resourceType = SecurityGroupVO.class) diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupRulePriorityMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupRulePriorityMsg.java index e7df5fbc598..1f15dfb20f3 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupRulePriorityMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIUpdateSecurityGroupRulePriorityMsg.java @@ -7,6 +7,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/security-groups/{securityGroupUuid}/rules/priority/actions", @@ -15,6 +16,8 @@ isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) + public class APIUpdateSecurityGroupRulePriorityMsg extends APIMessage implements SecurityGroupMessage { public static class SecurityGroupRulePriorityAO { @APIParam(resourceType = SecurityGroupRuleVO.class, nonempty = true, required = true) diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIValidateSecurityGroupRuleMsg.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIValidateSecurityGroupRuleMsg.java index 055867116b6..0cb2c85064b 100644 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIValidateSecurityGroupRuleMsg.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIValidateSecurityGroupRuleMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/security-groups/{securityGroupUuid}/rules/validation", method = HttpMethod.GET, responseClass = APIValidateSecurityGroupRuleReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIValidateSecurityGroupRuleMsg extends APISyncCallMessage { @APIParam(resourceType = SecurityGroupVO.class, required = true, nonempty = true) diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIAddSftpBackupStorageMsg.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIAddSftpBackupStorageMsg.java index 80f94f1fcd2..6d71b7de7ea 100755 --- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIAddSftpBackupStorageMsg.java +++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIAddSftpBackupStorageMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.APIAddBackupStorageMsg; +import org.zstack.header.vm.MetadataImpact; /** * @api * add a sftp backup storage @@ -61,6 +62,7 @@ responseClass = APIAddSftpBackupStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSftpBackupStorageMsg extends APIAddBackupStorageMsg { @APIParam(maxLength = 255, emptyString = false) private String hostname; diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIQuerySftpBackupStorageMsg.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIQuerySftpBackupStorageMsg.java index 4bf737b735f..b3c660111f1 100755 --- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIQuerySftpBackupStorageMsg.java +++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIQuerySftpBackupStorageMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQuerySftpBackupStorageReply.class, inventoryClass = SftpBackupStorageInventory.class) @RestRequest( @@ -16,6 +17,7 @@ method = HttpMethod.GET, responseClass = APIQuerySftpBackupStorageReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySftpBackupStorageMsg extends APIQueryMessage { diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIReconnectSftpBackupStorageMsg.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIReconnectSftpBackupStorageMsg.java index d8acf71130e..fb07bfc451d 100755 --- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIReconnectSftpBackupStorageMsg.java +++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIReconnectSftpBackupStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.BackupStorageMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/backup-storage/sftp/{uuid}/actions", @@ -13,6 +14,7 @@ responseClass = APIReconnectSftpBackupStorageEvent.class, method = HttpMethod.PUT ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectSftpBackupStorageMsg extends APIMessage implements BackupStorageMessage { @APIParam(resourceType = SftpBackupStorageVO.class) private String uuid; diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIUpdateSftpBackupStorageMsg.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIUpdateSftpBackupStorageMsg.java index 882a2a25a17..746615069b2 100755 --- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIUpdateSftpBackupStorageMsg.java +++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/APIUpdateSftpBackupStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.APIUpdateBackupStorageEvent; import org.zstack.header.storage.backup.APIUpdateBackupStorageMsg; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -16,6 +17,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateSftpBackupStorageMsg extends APIUpdateBackupStorageMsg { @APIParam(maxLength = 255, required = false) private String username; diff --git a/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/APIAddSharedMountPointPrimaryStorageMsg.java b/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/APIAddSharedMountPointPrimaryStorageMsg.java index 832d167aac6..42718e0cdf0 100755 --- a/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/APIAddSharedMountPointPrimaryStorageMsg.java +++ b/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/APIAddSharedMountPointPrimaryStorageMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.storage.primary.APIAddPrimaryStorageMsg; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.MetadataImpact; /** * Created by xing5 on 2016/3/27. @@ -17,6 +18,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSharedMountPointPrimaryStorageMsg extends APIAddPrimaryStorageMsg { public APIAddSharedMountPointPrimaryStorageMsg() { this.setType(SMPConstants.SMP_TYPE); diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIAttachSshKeyPairToVmInstanceMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIAttachSshKeyPairToVmInstanceMsg.java index e6281f7679a..aa2c1610248 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIAttachSshKeyPairToVmInstanceMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIAttachSshKeyPairToVmInstanceMsg.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.MetadataImpact; @RestRequest( path = "/ssh-key-pair/{sshKeyPairUuid}/vm-instance/{vmInstanceUuid}", @@ -12,6 +13,7 @@ responseClass = APIAttachSshKeyPairToVmInstanceEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAttachSshKeyPairToVmInstanceMsg extends APIMessage implements SshKeyPairMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APICreateSshKeyPairMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APICreateSshKeyPairMsg.java index 0f3b67ecc49..e03b1bd8ad2 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APICreateSshKeyPairMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APICreateSshKeyPairMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.tag.TagResourceType; import org.zstack.header.volume.VolumeVO; import org.zstack.sshkeypair.SshKeyPairConstant; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(VolumeVO.class) @RestRequest( @@ -15,6 +16,7 @@ responseClass = APICreateSshKeyPairEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateSshKeyPairMsg extends APICreateMessage { @APIParam(maxLength = 255, validRegexValues = SshKeyPairConstant.SSH_KEY_PAIR_NAME_REGEX) private String name; diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDeleteSshKeyPairMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDeleteSshKeyPairMsg.java index b15b8160f47..210b6196f1b 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDeleteSshKeyPairMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDeleteSshKeyPairMsg.java @@ -6,12 +6,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ssh-key-pair/{uuid}", method = HttpMethod.DELETE, responseClass = APIDeleteSshKeyPairEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteSshKeyPairMsg extends APIMessage implements SshKeyPairMessage, APIAuditor { @APIParam(resourceType = SshKeyPairVO.class) private String uuid; diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDetachSshKeyPairFromVmInstanceMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDetachSshKeyPairFromVmInstanceMsg.java index e2e1465795a..8b1335a7891 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDetachSshKeyPairFromVmInstanceMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIDetachSshKeyPairFromVmInstanceMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ssh-key-pair/{sshKeyPairUuid}/vm-instance/{vmInstanceUuid}", method = HttpMethod.DELETE, responseClass = APIDetachSshKeyPairFromVmInstanceEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDetachSshKeyPairFromVmInstanceMsg extends APIMessage implements SshKeyPairMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIGenerateSshKeyPairMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIGenerateSshKeyPairMsg.java index 1e3e368d59d..b5887318d8d 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIGenerateSshKeyPairMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIGenerateSshKeyPairMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.tag.TagResourceType; import org.zstack.header.volume.VolumeVO; import org.zstack.sshkeypair.SshKeyPairConstant; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(VolumeVO.class) @RestRequest( @@ -14,6 +15,7 @@ responseClass = APIGenerateSshKeyPairReply.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGenerateSshKeyPairMsg extends APISyncCallMessage { @APIParam(maxLength = 255, validRegexValues = SshKeyPairConstant.SSH_KEY_PAIR_NAME_REGEX) private String name; diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIQuerySshKeyPairMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIQuerySshKeyPairMsg.java index fafb3949bfd..4f7778ed918 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIQuerySshKeyPairMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIQuerySshKeyPairMsg.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQuerySshKeyPairReply.class, inventoryClass = SshKeyPairInventory.class) @RestRequest( @@ -15,6 +16,7 @@ responseClass = APIQuerySshKeyPairReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQuerySshKeyPairMsg extends APIQueryMessage { public static List __example__() { return Collections.singletonList("uuid=" + uuid()); diff --git a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIUpdateSshKeyPairMsg.java b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIUpdateSshKeyPairMsg.java index bfcfb307759..fb8c6f8b756 100644 --- a/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIUpdateSshKeyPairMsg.java +++ b/plugin/sshKeyPair/src/main/java/org/zstack/header/sshkeypair/APIUpdateSshKeyPairMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.sshkeypair.SshKeyPairConstant; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/ssh-key-pair/{uuid}/actions", @@ -14,6 +15,7 @@ responseClass = APIUpdateSshKeyPairEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateSshKeyPairMsg extends APIMessage implements SshKeyPairMessage, APIAuditor { @APIParam(resourceType = SshKeyPairVO.class, maxLength = 32) private String uuid; diff --git a/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/header/APICreateL2TfNetworkMsg.java b/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/header/APICreateL2TfNetworkMsg.java index 85a9c2b08f6..dfbcd7cc29a 100644 --- a/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/header/APICreateL2TfNetworkMsg.java +++ b/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/header/APICreateL2TfNetworkMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.network.l2.APICreateL2NetworkMsg; import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; @OverriddenApiParams({ @OverriddenApiParam(field = "physicalInterface", param = @APIParam(maxLength = 1024, required = false)), @@ -20,6 +21,7 @@ responseClass = APICreateL2NetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2TfNetworkMsg extends APICreateL2NetworkMsg { @APIParam(required = false, maxLength = 255) diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIChangeVipStateMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIChangeVipStateMsg.java index 9ff28c48729..e0462ba2c2a 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIChangeVipStateMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIChangeVipStateMsg.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.MetadataImpact; /** */ @@ -13,6 +14,7 @@ responseClass = APIChangeVipStateEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIChangeVipStateMsg extends APIMessage implements VipMessage { @APIParam(resourceType = VipVO.class) private String uuid; diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APICheckVipPortAvailabilityMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APICheckVipPortAvailabilityMsg.java index 47672f44076..1278fa25650 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APICheckVipPortAvailabilityMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APICheckVipPortAvailabilityMsg.java @@ -3,12 +3,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.message.APIGetMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vips/{vipUuid}/check-port-availability", method = HttpMethod.GET, responseClass = APICheckVipPortAvailabilityReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICheckVipPortAvailabilityMsg extends APIGetMessage { @APIParam(resourceType = VipVO.class) diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APICreateVipMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APICreateVipMsg.java index 657dac182ab..846c1325142 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APICreateVipMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APICreateVipMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api @@ -55,6 +56,7 @@ responseClass = APICreateVipEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVipMsg extends APICreateMessage implements L3NetworkMessage, IpAllocateMessage, APIAuditor { /** * @desc max length of 255 characters diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIDeleteVipMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIDeleteVipMsg.java index 0d183b34c2e..fbd7b2f5311 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIDeleteVipMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIDeleteVipMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkMessage; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @api delete vip @@ -38,6 +39,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVipEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVipMsg extends APIDeleteMessage implements L3NetworkMessage, VipMessage { /** * @ignore diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIGetVipAvailablePortMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIGetVipAvailablePortMsg.java index 95508023302..aff64542e1c 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIGetVipAvailablePortMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIGetVipAvailablePortMsg.java @@ -3,12 +3,14 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.message.APIGetMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vips/{vipUuid}/get-port-availability", method = HttpMethod.GET, responseClass = APIGetVipAvailablePortReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVipAvailablePortMsg extends APIGetMessage { @APIParam(resourceType = VipVO.class) diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIQueryVipMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIQueryVipMsg.java index 0613a9a4bfb..cb6ab85b694 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIQueryVipMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIQueryVipMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created with IntelliJ IDEA. @@ -22,6 +23,7 @@ method = HttpMethod.GET, responseClass = APIQueryVipReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVipMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIUpdateVipMsg.java b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIUpdateVipMsg.java index ede5bcd8f44..1ad44db5421 100755 --- a/plugin/vip/src/main/java/org/zstack/network/service/vip/APIUpdateVipMsg.java +++ b/plugin/vip/src/main/java/org/zstack/network/service/vip/APIUpdateVipMsg.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.MetadataImpact; /** * Created by frank on 6/15/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdateVipEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVipMsg extends APIMessage implements VipMessage { @APIParam(resourceType = VipVO.class) private String uuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterOfferingMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterOfferingMsg.java index cefe3c3ef0a..f165ae114d9 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterOfferingMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterOfferingMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.tag.TagResourceType; import org.zstack.header.zone.ZoneVO; +import org.zstack.header.vm.MetadataImpact; @TagResourceType(InstanceOfferingVO.class) @RestRequest( @@ -18,6 +19,7 @@ parameterName = "params", method = HttpMethod.POST ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVirtualRouterOfferingMsg extends APICreateInstanceOfferingMsg { @APIParam(resourceType = ZoneVO.class) private String zoneUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterVmMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterVmMsg.java index 314d5d1c910..055be5d1f43 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterVmMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APICreateVirtualRouterVmMsg.java @@ -9,6 +9,7 @@ import java.util.Set; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /* @RestRequest( @@ -18,6 +19,7 @@ parameterName = "params" ) */ +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVirtualRouterVmMsg extends APICreateVmInstanceMsg { @APIParam(resourceType = L3NetworkVO.class) private String managementNetworkUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetAttachablePublicL3ForVRouterMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetAttachablePublicL3ForVRouterMsg.java index 68d9b60503c..5bc29be4cb6 100644 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetAttachablePublicL3ForVRouterMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetAttachablePublicL3ForVRouterMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/vm-instances/appliances/virtual-routers/{vmInstanceUuid}/attachable-public-l3s", method = HttpMethod.GET, responseClass = APIGetAttachablePublicL3ForVRouterReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetAttachablePublicL3ForVRouterMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VirtualRouterVmVO.class) private String vmInstanceUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetVipUsedPortsMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetVipUsedPortsMsg.java index 52de35df050..fe7bfcfe8f9 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetVipUsedPortsMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIGetVipUsedPortsMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.network.service.vip.VipVO; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -13,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVipUsedPortsReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetVipUsedPortsMsg extends APISyncCallMessage { @APIParam(resourceType = VipVO.class) private String uuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIProvisionVirtualRouterConfigMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIProvisionVirtualRouterConfigMsg.java index a4c98b77e15..37f78131c99 100644 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIProvisionVirtualRouterConfigMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIProvisionVirtualRouterConfigMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -14,6 +15,7 @@ responseClass = APIProvisionVirtualRouterConfigEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIProvisionVirtualRouterConfigMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VirtualRouterVmVO.class) private String vmInstanceUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterOfferingMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterOfferingMsg.java index 58f4c4736c2..cbc607f017c 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterOfferingMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterOfferingMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; @AutoQuery(replyClass = APIQueryVirtualRouterOfferingReply.class, inventoryClass = VirtualRouterOfferingInventory.class) @RestRequest( @@ -16,6 +17,7 @@ responseClass = APIQueryVirtualRouterOfferingReply.class, method = HttpMethod.GET ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVirtualRouterOfferingMsg extends APIQueryMessage { diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterVmMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterVmMsg.java index 3a23782fe6b..0a29f86f6f9 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterVmMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIQueryVirtualRouterVmMsg.java @@ -9,6 +9,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryApplianceVmReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVirtualRouterVmMsg extends APIQueryApplianceVmMsg { public static List __example__() { diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIReconnectVirtualRouterMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIReconnectVirtualRouterMsg.java index 38cb76b31cc..590c8a4cd1e 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIReconnectVirtualRouterMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIReconnectVirtualRouterMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.MetadataImpact; /** */ @@ -14,6 +15,7 @@ responseClass = APIReconnectVirtualRouterEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIReconnectVirtualRouterMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VirtualRouterVmVO.class) private String vmInstanceUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterMsg.java index dfb2f4db2ae..5c931053a28 100644 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.MetadataImpact; /** * Created by shixin.ruan 2020/02/12. @@ -16,6 +17,7 @@ isAction = true, responseClass = APIUpdateVirtualRouterEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVirtualRouterMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VirtualRouterVmVO.class) private String vmInstanceUuid; diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterOfferingMsg.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterOfferingMsg.java index 87467ff6095..341fe95c9ba 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterOfferingMsg.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/APIUpdateVirtualRouterOfferingMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.image.ImageVO; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by frank on 7/31/2015. @@ -16,6 +17,7 @@ method = HttpMethod.PUT, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVirtualRouterOfferingMsg extends APIUpdateInstanceOfferingMsg { private Boolean isDefault; @APIParam(resourceType = ImageVO.class, required = false) diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APICreateVxlanVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APICreateVxlanVtepMsg.java index a1cc5a81037..c740bf2a38e 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APICreateVxlanVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APICreateVxlanVtepMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l2.L2NetworkMessage; import org.zstack.header.rest.RestRequest; import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanNetworkPoolVO; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/l2-networks/vxlan/vteps", @@ -13,6 +14,7 @@ responseClass = APICreateVxlanVtepEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVxlanVtepMsg extends APICreateMessage implements L2NetworkMessage { @APIParam(resourceType = HostVO.class) private String hostUuid; diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APIQueryVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APIQueryVtepMsg.java index 24426a14f77..a59961e9a30 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APIQueryVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vtep/APIQueryVtepMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 27/05/2017. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryVtepReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVtepMsg extends APIQueryMessage { public static List __example__() { return asList(); diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APICreateL2VxlanNetworkMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APICreateL2VxlanNetworkMsg.java index 141aa30719e..c4772a2c73b 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APICreateL2VxlanNetworkMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APICreateL2VxlanNetworkMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.zone.ZoneVO; import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanNetworkPoolVO; +import org.zstack.header.vm.MetadataImpact; @OverriddenApiParams({ @OverriddenApiParam(field = "physicalInterface", param = @APIParam(maxLength = 1024, required = false)), @@ -19,6 +20,7 @@ responseClass = APICreateL2VxlanNetworkEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2VxlanNetworkMsg extends APICreateL2NetworkMsg { @APIParam(required = false, numberRange = {1, 16777214}) private Integer vni; diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APIQueryL2VxlanNetworkMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APIQueryL2VxlanNetworkMsg.java index a7aaf9b01c8..48de03da585 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APIQueryL2VxlanNetworkMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetwork/APIQueryL2VxlanNetworkMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 15/03/2017. @@ -20,6 +21,7 @@ method = HttpMethod.GET, responseClass = APIQueryL2VxlanNetworkReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryL2VxlanNetworkMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateL2VxlanNetworkPoolMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateL2VxlanNetworkPoolMsg.java index 9ccac6077d0..dda902e183b 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateL2VxlanNetworkPoolMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateL2VxlanNetworkPoolMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.OverriddenApiParams; import org.zstack.header.network.l2.APICreateL2NetworkMsg; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @OverriddenApiParams({ @OverriddenApiParam(field = "physicalInterface", param = @APIParam(maxLength = 1024, required = false)) @@ -16,6 +17,7 @@ responseClass = APICreateL2VxlanNetworkPoolEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateL2VxlanNetworkPoolMsg extends APICreateL2NetworkMsg { @Override diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVniRangeMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVniRangeMsg.java index b48abcd0857..872265877e3 100755 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVniRangeMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVniRangeMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.network.l2.L2NetworkMessage; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 09/03/2017. @@ -18,6 +19,7 @@ responseClass = APICreateVniRangeEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVniRangeMsg extends APICreateMessage implements L2NetworkMessage, APIAuditor { @APIParam(maxLength = 255) diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java index 6871cb12344..385de0efe48 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.network.l2.L2NetworkMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( @@ -13,6 +14,7 @@ responseClass = APICreateVxlanPoolRemoteVtepEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APICreateVxlanPoolRemoteVtepMsg extends APICreateMessage implements L2NetworkMessage { @APIParam diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVniRangeMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVniRangeMsg.java index d7ff35489ea..7f77ac16f9f 100755 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVniRangeMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVniRangeMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 03/05/2017. @@ -19,6 +20,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVniRangeEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVniRangeMsg extends APIDeleteMessage implements L2NetworkMessage, APIAuditor { @APIParam(resourceType = VniRangeVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java index c8b8e13807f..0515383fbe7 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.network.l2.L2NetworkMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/l2-networks/{l2NetworkUuid}/clusters/{clusterUuid}/delete/remote-vtep-ip", method = HttpMethod.DELETE, responseClass = APIDeleteVxlanPoolRemoteVtepEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIDeleteVxlanPoolRemoteVtepMsg extends APIDeleteMessage implements L2NetworkMessage { @APIParam diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryL2VxlanNetworkPoolMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryL2VxlanNetworkPoolMsg.java index 2661e9665a5..bacd6ac78ea 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryL2VxlanNetworkPoolMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryL2VxlanNetworkPoolMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 15/03/2017. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryL2VxlanNetworkPoolReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryL2VxlanNetworkPoolMsg extends APIQueryMessage { public static List __example__() { diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryVniRangeMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryVniRangeMsg.java index c7fef02c711..fed3a52444b 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryVniRangeMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIQueryVniRangeMsg.java @@ -8,6 +8,7 @@ import java.util.List; import static java.util.Arrays.asList; +import org.zstack.header.vm.MetadataImpact; /** * Created by weiwang on 15/03/2017. @@ -19,6 +20,7 @@ method = HttpMethod.GET, responseClass = APIQueryVniRangeReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryVniRangeMsg extends APIQueryMessage { public static List __example__() { return asList(); diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIUpdateVniRangeMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIUpdateVniRangeMsg.java index 35bd3ca1a90..fed74c547eb 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIUpdateVniRangeMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIUpdateVniRangeMsg.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.MetadataImpact; /** * @author: kefeng.wang @@ -19,6 +20,7 @@ responseClass = APIUpdateVniRangeEvent.class, isAction = true ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateVniRangeMsg extends APIMessage implements L2NetworkMessage, APIAuditor { @APIParam(resourceType = VniRangeVO.class) private String uuid; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceBindableConfigMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceBindableConfigMsg.java index dd8d92b7bb5..b70e857691b 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceBindableConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceBindableConfigMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2019/2/23. @@ -11,6 +12,7 @@ @RestRequest(path = "/resource-configurations/bindable", optionalPaths = {"/resource-configurations/bindable/{category}"}, method = HttpMethod.GET, responseClass = APIGetResourceBindableConfigReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetResourceBindableConfigMsg extends APISyncCallMessage { @APIParam(required = false) private String category; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigMsg.java index 1133a6b8a3c..3c068844e22 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vm.MetadataImpact; /** * Created by MaJin on 2019/2/23. @@ -13,6 +14,7 @@ @RestRequest(path = "/resource-configurations/{resourceUuid}/{category}/{name}", method = HttpMethod.GET, responseClass = APIGetResourceConfigReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetResourceConfigMsg extends APISyncCallMessage implements ResourceConfigMessage { @APIParam private String category; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigsMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigsMsg.java index 7950613273d..e5826b114bb 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigsMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIGetResourceConfigsMsg.java @@ -8,9 +8,11 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/resource-configurations/{resourceUuid}/{category}", method = HttpMethod.GET, responseClass = APIGetResourceConfigsReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIGetResourceConfigsMsg extends APISyncCallMessage implements ResourceConfigMessage { @APIParam private String category; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIQueryResourceConfigMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIQueryResourceConfigMsg.java index a5c8f646015..22b9a08f023 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIQueryResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIQueryResourceConfigMsg.java @@ -7,9 +7,11 @@ import java.util.Collections; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/resource-configurations", method = HttpMethod.GET, responseClass = APIQueryResourceConfigReply.class) @AutoQuery(replyClass = APIQueryResourceConfigReply.class, inventoryClass = ResourceConfigInventory.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIQueryResourceConfigMsg extends APIQueryMessage { public static List __example__() { return Collections.singletonList("category=host"); diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigsMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigsMsg.java index 74a6c261d0d..0e2169b50c7 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigsMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigsMsg.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.List; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/resource-configurations/{resourceUuid}/resource-configs/actions", @@ -16,6 +17,7 @@ parameterName = "params", responseClass = APIUpdateResourceConfigsEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIUpdateResourceConfigsMsg extends APIMessage { @PythonClassInventory public static class ResourceConfigAO { diff --git a/search/src/main/java/org/zstack/query/APIBatchQueryMsg.java b/search/src/main/java/org/zstack/query/APIBatchQueryMsg.java index 0ad91850f93..0235e89926d 100755 --- a/search/src/main/java/org/zstack/query/APIBatchQueryMsg.java +++ b/search/src/main/java/org/zstack/query/APIBatchQueryMsg.java @@ -3,9 +3,11 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/batch-queries", method = HttpMethod.GET, responseClass = APIBatchQueryReply.class) @Deprecated +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIBatchQueryMsg extends APISyncCallMessage { private String script; diff --git a/search/src/main/java/org/zstack/query/APIZQLQueryMsg.java b/search/src/main/java/org/zstack/query/APIZQLQueryMsg.java index 7c153eaa25a..14795806507 100755 --- a/search/src/main/java/org/zstack/query/APIZQLQueryMsg.java +++ b/search/src/main/java/org/zstack/query/APIZQLQueryMsg.java @@ -3,8 +3,10 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest(path = "/zql", method = HttpMethod.GET, responseClass = APIZQLQueryReply.class) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIZQLQueryMsg extends APISyncCallMessage { private String zql; diff --git a/search/src/main/java/org/zstack/search/APIRefreshSearchIndexesMsg.java b/search/src/main/java/org/zstack/search/APIRefreshSearchIndexesMsg.java index 10064236fa8..25ca14205dc 100644 --- a/search/src/main/java/org/zstack/search/APIRefreshSearchIndexesMsg.java +++ b/search/src/main/java/org/zstack/search/APIRefreshSearchIndexesMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APISyncCallMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; /** * @ Author : yh.w @@ -12,6 +13,7 @@ method = HttpMethod.GET, responseClass = APIRefreshSearchIndexesReply.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIRefreshSearchIndexesMsg extends APISyncCallMessage { public static APIRefreshSearchIndexesMsg __example__() { diff --git a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/APIAddSimulatorHostMsg.java b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/APIAddSimulatorHostMsg.java index 9b90fcb0ed2..70c0fa2317b 100755 --- a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/APIAddSimulatorHostMsg.java +++ b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/APIAddSimulatorHostMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.host.APIAddHostMsg; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/hosts/simulators", @@ -12,6 +13,7 @@ parameterName = "params", responseClass = APIAddHostEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSimulatorHostMsg extends APIAddHostMsg { @APIParam private long memoryCapacity = 1000000000; diff --git a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/backup/APIAddSimulatorBackupStorageMsg.java b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/backup/APIAddSimulatorBackupStorageMsg.java index 57f741be93d..724c2ef6619 100755 --- a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/backup/APIAddSimulatorBackupStorageMsg.java +++ b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/backup/APIAddSimulatorBackupStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.backup.APIAddBackupStorageEvent; import org.zstack.header.storage.backup.APIAddBackupStorageMsg; +import org.zstack.header.vm.MetadataImpact; @RestRequest( @@ -12,6 +13,7 @@ parameterName = "params", responseClass = APIAddBackupStorageEvent.class ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSimulatorBackupStorageMsg extends APIAddBackupStorageMsg { private long totalCapacity; private long availableCapacity; diff --git a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/primary/APIAddSimulatorPrimaryStorageMsg.java b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/primary/APIAddSimulatorPrimaryStorageMsg.java index ab5739c5ed3..dadae84696d 100755 --- a/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/primary/APIAddSimulatorPrimaryStorageMsg.java +++ b/simulator/simulatorHeader/src/main/java/org/zstack/header/simulator/storage/primary/APIAddSimulatorPrimaryStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.APIAddPrimaryStorageEvent; import org.zstack.header.storage.primary.APIAddPrimaryStorageMsg; +import org.zstack.header.vm.MetadataImpact; @RestRequest( path = "/primary-storage/simulators", @@ -11,6 +12,7 @@ responseClass = APIAddPrimaryStorageEvent.class, parameterName = "params" ) +@MetadataImpact(MetadataImpact.Impact.NONE) public class APIAddSimulatorPrimaryStorageMsg extends APIAddPrimaryStorageMsg { private long totalCapacity = 100000000; private long availableCapacity = 10000000;