-
Notifications
You must be signed in to change notification settings - Fork 0
<feature>[sdk]: expose volumeUuids on PrimaryStorageMigrateVolumeAction #4022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: zsv_5.1.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # ZSPHER-244 API 设计权衡:扩展现有 API vs 新增 API | ||
|
|
||
| > 关联工单:[ZSPHER-244](http://jira.zstack.io/browse/ZSPHER-244) — 更改数据存储支持数据盘指定不同目标 | ||
| > 关联前置:[ZSV-12280](http://jira.zstack.io/browse/ZSV-12280) — SharedBlock 多数据盘热迁移 | ||
| > 状态:设计阶段 | ||
| > 决策建议:方案 B(新增独立 API) | ||
|
|
||
| --- | ||
|
|
||
| ## 1. 背景 | ||
|
|
||
| `APIPrimaryStorageMigrateVolumeMsg` 当前承担的语义已经叠了两层: | ||
|
|
||
| | 演进阶段 | 字段 | 适用场景 | | ||
| |---------|------|---------| | ||
| | 最早 | `volumeUuid`(path) + `dstPrimaryStorageUuid` | 单盘冷迁移(VM Stopped 或未挂载) | | ||
| | ZSV-12280(已落) | `volumeUuids: List<String>` + `dstPrimaryStorageUuid` | 多盘热迁移(VM Running/Paused,**统一目标 PS**) | | ||
| | ZSPHER-244(待做) | 每盘独立目标:`List<{volumeUuid, dstPsUuid}>` | 多盘**冷迁移**(VM Stopped,**每盘不同目标**) | | ||
|
|
||
| 三种语义共用一个 API 已经出现"互斥字段"问题(`volumeUuid` 与 `volumeUuids` 必须二选一/包含关系)。ZSPHER-244 是否继续叠加,需要明确决策。 | ||
|
|
||
| --- | ||
|
|
||
| ## 2. 方案 A:继续给 `APIPrimaryStorageMigrateVolumeMsg` 加参数 | ||
|
|
||
| 新增字段 `volumeMigrationSpecs: List<VolumeMigrationSpec{volumeUuid, dstPrimaryStorageUuid}>`。 | ||
|
|
||
| ### 优点 | ||
| - 前端入口统一(一个 API 处理 volume 迁移所有场景) | ||
| - SDK 兼容:旧字段保留 | ||
| - LongJob 调度复用现有 `PrimaryStorageMigrateVolumeJob` | ||
|
|
||
| ### 缺点(多数已经显现) | ||
| 1. **互斥字段爆炸**:`volumeUuid` / `volumeUuids` / `volumeMigrationSpecs` / `dstPrimaryStorageUuid` 之间需要 5+ 条互斥规则,Interceptor 校验逻辑将剧增。 | ||
| 2. **语义模糊**:同一 API 既是热迁移又是冷迁移,既支持单目标又支持多目标——文档和 SDK 注释难写。 | ||
| 3. **路径占位符 `volumeUuid` 越来越尴尬**:当请求是「按盘指定目标」时,path 上的 volumeUuid 是哪一块?需要继续打补丁说"必须是 specs 中的某一块"。 | ||
| 4. **审计/事件 inventory 形状不一致**:单目标返回 `VolumeInventory`,多目标返回 `List<VolumeInventory>`。老调用方期望单个,新调用方期望列表,已经在用 `inventories.get(0)` 凑活。 | ||
| 5. **MN 升级风险**:当前 `PrimaryStorageMigrateVolumeJob.start` 用 `JSONObjectUtil.toObject` 反序列化 jobData,加字段虽兼容但分支判断越来越复杂。 | ||
|
|
||
| --- | ||
|
|
||
| ## 3. 方案 B:新增独立 API `APIPrimaryStorageMigrateDataVolumesMsg` | ||
|
|
||
| 专门承载 ZSPHER-244 语义: | ||
|
|
||
| ```java | ||
| @Action(category = VolumeConstant.ACTIONS) | ||
| public class APIPrimaryStorageMigrateDataVolumesMsg extends APIMessage { | ||
| @APIParam(resourceType = VmInstanceVO.class) | ||
| private String vmInstanceUuid; | ||
|
|
||
| @APIParam(nonempty = true) | ||
| private List<VolumeMigrationSpec> volumeMigrationSpecs; | ||
| // VolumeMigrationSpec: volumeUuid + dstPrimaryStorageUuid (+ optional withSnapshots) | ||
| } | ||
| ``` | ||
|
|
||
| ### 优点 | ||
| 1. **语义干净**:一个 API 一种语义,文档/SDK/审计天然清晰。 | ||
| 2. **Interceptor 简单**:只校验自己的字段,与老 API 完全解耦。 | ||
| 3. **入口分流**:冷迁移多目标走新 Msg → 新 Job(`PrimaryStorageMigrateDataVolumesJob`);老 API 不动,回归"经典单盘迁移"角色。 | ||
| 4. **Inventory 形状自然**:新事件直接返回 `List<VolumeInventory>`,不用兼容老形状。 | ||
| 5. **演进空间**:未来 ZSPHER-244 的衍生需求(按盘选 withSnapshots、按盘选 strategy 等)只在新 Msg 上扩展,不污染老路径。 | ||
| 6. **可独立灰度**:新 API 出问题不影响存量。 | ||
|
|
||
| ### 缺点 | ||
| 1. 前端要多一个 API 调用入口(但 UI 本来就是新的高级选项面板,天然走新接口)。 | ||
| 2. SDK 需要重新生成(项目里本来就是必经流程)。 | ||
| 3. 多了一个 LongJob 类(但本来就需要新逻辑,写在哪里都一样)。 | ||
|
|
||
| --- | ||
|
|
||
| ## 4. 同类先例参考 | ||
|
|
||
| ZStack 自身的 API 演进经验: | ||
|
|
||
| - `APIMigrateVmMsg`(冷)/ `APILiveMigrateVmMsg`(热)—— **是分开的两个 API**,没有共用 `migrateVm` 加 `live: boolean`。 | ||
| - `APIChangeVmHaPolicyMsg` / `APISetVmInstanceDefaultCdRomMsg` 这类语义独立的操作,从不强行复用旧 API。 | ||
| - 当年 `volumeUuids` 加到 `APIPrimaryStorageMigrateVolumeMsg` 已经是历史包袱(即 ZSV-12280 当前进行中的工作),现在如果再叠一层 multi-target,会让这个 API 彻底变成"什么都能干的 god API"。 | ||
|
|
||
| --- | ||
|
|
||
| ## 5. 推荐:方案 B(新增独立 API) | ||
|
|
||
| ### 关键理由 | ||
| 1. ZSPHER-244 是 **冷迁移 + 每盘独立目标**,与 ZSV-12280 的 **热迁移 + 统一目标** 实际是两条完全独立的执行链路: | ||
| - 冷迁移:`MigrateVolumeOnPrimaryStorageMsg` fan-out(每盘可走不同目标 PS) | ||
| - 热迁移:KVM live storage migration workflow(per-volume LUN switch) | ||
|
|
||
| 共用 API 只是表面省事,后端早晚要按字段分流。 | ||
|
|
||
| 2. **Interceptor 复杂度上限**:现在 `validateLiveMigrateMultiDataVolumes` 已经接近 80 行,再叠一种语义会到 150+ 行,可维护性崩盘。 | ||
|
|
||
| 3. **老 API 已经背了一个互斥规则补丁**(path `volumeUuid` 必须 ∈ body `volumeUuids`),再加规则会让前端联调和文档维护痛苦。 | ||
|
|
||
| ### 建议落地形态 | ||
|
|
||
| - 新增 `APIPrimaryStorageMigrateDataVolumesMsg` + `APIPrimaryStorageMigrateDataVolumesEvent` | ||
| - 新增 `PrimaryStorageMigrateDataVolumesJob`(LongJob),内部串行/并行调用现有 `MigrateVolumeOnPrimaryStorageMsg`(每盘一个目标 PS) | ||
| - Interceptor 单独写 `validate(APIPrimaryStorageMigrateDataVolumesMsg)`,与 multi-volume 热迁移校验完全分离 | ||
| - 老 `APIPrimaryStorageMigrateVolumeMsg.volumeUuids` 路径继续仅服务 ZSV-12280 热迁移场景,不改动 | ||
|
|
||
| ### 边界情况 | ||
| - 如果 PD 要求"按盘选目标"也能用于**热迁移**,再单独评估是否要在新 API 上加 `live: boolean`,或者再开一个 `APILiveMigrateDataVolumesToDifferentTargets`。 | ||
| - 但 ZSPHER-244 原文明确写「SAN ↔ SAN **冷迁移**」,目前不必预留。 | ||
|
|
||
| --- | ||
|
|
||
| ## 6. 决策待办 | ||
|
|
||
| - [ ] 与 PD 确认 ZSPHER-244 是否仅冷迁移场景 | ||
| - [ ] 与前端确认是否接受新 API(UI 本身就是新面板,预计无阻力) | ||
| - [ ] 与架构组评审字段命名 `volumeMigrationSpecs` vs `volumeTargets` | ||
| - [ ] 确认审计 inventory 形状(List 返回是否需要追加 `failedVolumes` 字段) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package org.zstack.sdk; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import org.zstack.sdk.*; | ||
|
|
||
| public class PrimaryStorageMigrateDataVolumesAction extends AbstractAction { | ||
|
|
||
| private static final HashMap<String, Parameter> parameterMap = new HashMap<>(); | ||
|
|
||
| private static final HashMap<String, Parameter> nonAPIParameterMap = new HashMap<>(); | ||
|
|
||
| public static class Result { | ||
| public ErrorCode error; | ||
| public org.zstack.sdk.PrimaryStorageMigrateDataVolumesResult value; | ||
|
|
||
| public Result throwExceptionIfError() { | ||
| if (error != null) { | ||
| throw new ApiException( | ||
| String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) | ||
| ); | ||
| } | ||
|
|
||
| return this; | ||
| } | ||
| } | ||
|
|
||
| @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) | ||
| public java.lang.String vmInstanceUuid; | ||
|
|
||
| @Param(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false) | ||
| public java.util.List volumeMigrationSpecs; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 使用泛型类型参数以增强类型安全
🔧 建议修复 `@Param`(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false)
- public java.util.List volumeMigrationSpecs;
+ public java.util.List<VolumeMigrationSpec> volumeMigrationSpecs;🤖 Prompt for AI Agents |
||
|
|
||
| @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.PrimaryStorageMigrateDataVolumesResult value = res.getResult(org.zstack.sdk.PrimaryStorageMigrateDataVolumesResult.class); | ||
| ret.value = value == null ? new org.zstack.sdk.PrimaryStorageMigrateDataVolumesResult() : value; | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| public Result call() { | ||
| ApiResult res = ZSClient.call(this); | ||
| return makeResult(res); | ||
| } | ||
|
|
||
| public void call(final Completion<Result> completion) { | ||
| ZSClient.call(this, new InternalCompletion() { | ||
| @Override | ||
| public void complete(ApiResult res) { | ||
| completion.complete(makeResult(res)); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| protected Map<String, Parameter> getParameterMap() { | ||
| return parameterMap; | ||
| } | ||
|
|
||
| protected Map<String, Parameter> getNonAPIParameterMap() { | ||
| return nonAPIParameterMap; | ||
| } | ||
|
|
||
| protected RestInfo getRestInfo() { | ||
| RestInfo info = new RestInfo(); | ||
| info.httpMethod = "PUT"; | ||
| info.path = "/primary-storage/data-volumes/actions"; | ||
| info.needSession = true; | ||
| info.needPoll = true; | ||
| info.parameterName = "primaryStorageMigrateDataVolumes"; | ||
| return info; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.zstack.sdk; | ||
|
|
||
|
|
||
|
|
||
| public class PrimaryStorageMigrateDataVolumesResult { | ||
| public java.util.List inventories; | ||
| public void setInventories(java.util.List inventories) { | ||
| this.inventories = inventories; | ||
| } | ||
| public java.util.List getInventories() { | ||
| return this.inventories; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,9 +25,12 @@ public Result throwExceptionIfError() { | |
| } | ||
| } | ||
|
|
||
| @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) | ||
| @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) | ||
| public java.lang.String volumeUuid; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| @Param(required = false) | ||
| public java.util.List<String> volumeUuids; | ||
|
|
||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 缺少 当前两个字段都为可选,且没有本地校验,允许“两个都不传”或“两个都传”的无效状态进入请求层。建议增加 one-of 校验(至少一个且最好只允许一个),并对 可参考的最小校验补丁 public Result call() {
+ validateVolumeParams();
ApiResult res = ZSClient.call(this);
return makeResult(res);
}
public void call(final Completion<Result> completion) {
+ validateVolumeParams();
ZSClient.call(this, new InternalCompletion() {
`@Override`
public void complete(ApiResult res) {
completion.complete(makeResult(res));
}
});
}
+
+private void validateVolumeParams() {
+ boolean hasSingle = volumeUuid != null && !volumeUuid.isEmpty();
+ boolean hasBatch = volumeUuids != null && !volumeUuids.isEmpty();
+ if (!hasSingle && !hasBatch) {
+ throw new ApiException("either volumeUuid or volumeUuids must be provided");
+ }
+ if (hasSingle && hasBatch) {
+ throw new ApiException("volumeUuid and volumeUuids cannot be provided together");
+ }
+}🤖 Prompt for AI Agents |
||
| @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) | ||
| public java.lang.String dstPrimaryStorageUuid; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.zstack.sdk; | ||
|
|
||
| public class VolumeMigrationSpec { | ||
| public java.lang.String volumeUuid; | ||
| public java.lang.String dstPrimaryStorageUuid; | ||
|
|
||
| public void setVolumeUuid(java.lang.String volumeUuid) { | ||
| this.volumeUuid = volumeUuid; | ||
| } | ||
| public java.lang.String getVolumeUuid() { | ||
| return this.volumeUuid; | ||
| } | ||
|
|
||
| public void setDstPrimaryStorageUuid(java.lang.String dstPrimaryStorageUuid) { | ||
| this.dstPrimaryStorageUuid = dstPrimaryStorageUuid; | ||
| } | ||
| public java.lang.String getDstPrimaryStorageUuid() { | ||
| return this.dstPrimaryStorageUuid; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
将 Jira 链接统一为 HTTPS。
当前文档引用使用了
http://,建议改为https://以避免跳转或安全告警。建议修改
📝 Committable suggestion
🤖 Prompt for AI Agents