Skip to content

Conversation

zstack-robot-2
Copy link
Collaborator

Resolves: ZSTAC-77315

Change-Id: I75656f6178726675636f6d63656b666873786d62

sync from gitlab !8384

Copy link

coderabbitai bot commented Sep 9, 2025

Walkthrough

新增 IscsiRemoteTarget.fromUri(String) 用于解析 iscsi:// URI 并返回对象或 null;在 ExponStorageController 中为 hypervisor 为 baremetal2 的主机在 iSCSI 卷激活/撤销时改为基于卷/VM UUID 推导 IQN,并合并 volume 导入为通配符。

Changes

Cohort / File(s) Change Summary
iSCSI 目标 URI 解析
header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java
新增 public static IscsiRemoteTarget fromUri(String uriString):解析 scheme=iscsi 的 URI,校验 host/port,拆分 path 以获取 iqn 与 diskIdType/diskId;遇到非法 scheme/格式或异常时记录日志并返回 null;新增 URICLoggerUtils 等导入及私有静态 logger。
Expon 控制器 baremetal2 特例
plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
activeIscsiVolumedeactivateIscsi 分支中:若宿主机 hypervisorbaremetal2,则通过卷或安装路径解析出 VolumeVO/VM UUID,并基于该 UUID 推导 IQN;否则保持原有使用宿主机 initiator 的逻辑;同时将多个 volume 相关导入合并为 org.zstack.header.volume.*

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Parser as IscsiRemoteTarget.fromUri
  participant URI as URI.parse

  Caller->>Parser: fromUri(uriString)
  Parser->>URI: 解析 scheme/host/port/path
  alt 非 iscsi 或 路径/ID 格式非法 或 异常
    Parser-->>Caller: null(记录日志)
  else 合法
    Parser-->>Caller: IscsiRemoteTarget 实例
  end
Loading
sequenceDiagram
  autonumber
  participant Ctrl as ExponStorageController
  participant Host as Host
  participant Vol as Volume/VolumeVO
  participant iSCSI as iSCSI 服务

  Ctrl->>Host: 获取 hypervisor 类型
  alt hypervisor = baremetal2
    Ctrl->>Vol: 通过 volumeUuid 或 installPath 获取 VolumeVO
    Ctrl->>Ctrl: 基于 Volume/VM UUID 推导 IQN
  else 其他类型
    Ctrl->>Host: 读取宿主机 initiator IQN
  end
  Ctrl->>iSCSI: 激活/撤销 卷(使用选定 IQN)
  iSCSI-->>Ctrl: 返回结果
Loading

Estimated code review effort

🎯 3 (中等) | ⏱️ ~25 分钟

Poem

我是小兔解 URI,跳进 iscsi 的溪流,
主机若是 baremetal2,我从 UUID 找到它的瞳孔,
激活与撤销像两端的摇篮,
日志记录每一步轻声的足印,
我在草间竖耳,庆祝更新的小小欢腾。 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 该 PR 标题“[storage]: test”过于模糊,并未反映出新增 IscsiRemoteTarget.fromUri 方法或在 ExponStorageController 中对 baremetal2 IQN 解析的主要改动。 请将标题调整为简洁明了地概括主要改动,例如“新增 IscsiRemoteTarget.fromUri 方法并在 ExponStorageController 中支持 baremetal2 IQN 解析”。
Description Check ❓ Inconclusive PR 描述仅包含问题单编号、Change-Id 及同步来源等元数据,没有提供任何关于新增方法或逻辑修改的说明,过于笼统且缺乏实质信息。 请在 PR 描述中补充本次更改的背景、目的及具体代码改动要点,以便同事能够快速了解变更内容。
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sync/jin.shen/test-77315@@2

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java (1)

14-14: 布尔参数语义不直观,考虑用枚举表达模式(后续演进)

shareable 为布尔位,语义扩展性与可读性较弱。建议在后续版本引入枚举(如 VolumeShareMode { EXCLUSIVE, SHAREABLE })替代,并提供兼容的 default 适配层。当前 PR 可不阻塞,仅作为演进建议。

📜 Review details

Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4c53f and cf59c62.

📒 Files selected for processing (1)
  • storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.*

⚙️ CodeRabbit configuration file

**/*.*: - 代码里不应当有有中文,包括报错、注释等都应当使用正确的、无拼写错误的英文来写

Files:

  • storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: ## 1. API 设计要求

  • API 命名:
    • API 名称必须唯一,不能重复。
    • API 消息类需要继承 APIMessage;其返回类必须继承 APIReplyAPIEvent,并在注释中用 @RestResponse 进行标注。
    • API 消息上必须添加注解 @RestRequest,并满足如下规范:
      • path:
        • 针对资源使用复数形式。
        • 当 path 中引用消息类变量时,使用 {variableName} 格式。
      • HTTP 方法对应:
        • 查询操作 → HttpMethod.GET
        • 更新操作 → HttpMethod.PUT
        • 创建操作 → HttpMethod.POST
        • 删除操作 → HttpMethod.DELETE
    • API 类需要实现 __example__ 方法以便生成 API 文档,并确保生成对应的 Groovy API Template 与 API Markdown 文件。

2. 命名与格式规范

  • 类名:

    • 使用 UpperCamelCase 风格。
    • 特殊情况:
      • VO/AO/EO 类型类除外。
      • 抽象类采用 AbstractBase 前缀/后缀。
      • 异常类应以 Exception 结尾。
      • 测试类需要以 TestCase 结尾。
  • 方法名、参数名、成员变量和局部变量:

    • 使用 lowerCamelCase 风格。
  • 常量命名:

    • 全部大写,使用下划线分隔单词。
    • 要求表达清楚,避免使用含糊或不准确的名称。
  • 包名:

    • 统一使用小写,使用点分隔符,每个部分应是一个具有自然语义的英文单词(参考 Spring 框架的结构)。
  • 命名细节:

    • 避免在父子类或同一代码块中出现相同名字的成员或局部变量,防止混淆。
    • 命名缩写:
      • 不允许使用不必要的缩写,如:AbsSchedulerJobcondiFu 等。应使用完整单词提升可读性。

3. 编写自解释代码

  • 意图表达:

    • 避免使用布尔型参数造成含义不明确。例如:
      • 对于 stopAgent(boolean ignoreError),建议拆分为不同函数(如 stopAgentIgnoreError()),或使用枚举表达操作类型。
    • 命名应尽量用完整的单词组合表达意图,并在名称中体现数据类型或用途(例如在常量与变量名称中,将类型词放在末尾)。
    • 避免使用魔法值(Magic Value):
      • 直接使用未经定义的数值或字符串(如 if (status == 5))应替换为枚举或常量。

      • 示例:

      • // 错误示例:魔法值

      • if (user.getStatus() == 5) { ... }

      • // 正确示例:常量或枚举

      • public static final int STATUS_ACTIVE = 5;

      • if (user.getStatus() == STATUS_ACTIVE) { ... }

      • // 或使用枚举

      • enum UserStatus { ACTIVE, INACTIVE }

  • 注释:

    • 代码应尽量做到自解释,对少于两行的说明可以直接写在代码中。
    • 对于较长的注释,需要仔细校对并随代码更新,确保内容正确。
    • 接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。

4. 流程控制和结构优化

  • if...else 的使用:

    • 应尽量减少 if...else 结构的使用,建议:
      • 限制嵌套层级最多为两层,且内层不应再出现 else 分支。
      • 尽早返回(Early Return),将条件判断中的处理逻辑提前结束或抽成独立方法。
      • 使用 Java Stream 或 Lambda 表达式代替冗长的循环与条件判断。
  • 条件判断:

    • if 条件表达不宜过长或过于复杂,必要时可以将条件抽成 boolean 变量描述。
  • 代码块长度:
    ...

Files:

  • storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java
🔇 Additional comments (1)
storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java (1)

14-14: BlockExternalPrimaryStorageFactory 接口无实现或调用 仓库中未发现任何类实现该接口或调用 activeIscsiVolume,变更不会影响现有代码。

BlockExternalPrimaryStorageBackend getBlockExternalPrimaryStorageBackend(PrimaryStorageVO vo);

void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable);
void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO vo);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

为新增/变更的 API 方法补充英文 Javadoc,明确契约与可空性

根据规范“接口方法必须配有有效的 Javadoc 注释”,请为该方法补充用途、参数语义、异常与可空性约定(是否允许 null)。示例:

-    default void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO primaryStorage) {
+    /**
+     * Activate an iSCSI volume for the specified client IQN on a primary storage.
+     *
+     * @param clientIqn      iSCSI initiator IQN of the client
+     * @param vol            volume info to be activated
+     * @param shareable      whether the volume should be activated in shareable mode
+     * @param primaryStorage the target primary storage context; when omitted by legacy callers,
+     *                       implementations SHOULD derive it from the volume or ignore it safely
+     * @throws IllegalArgumentException if inputs are invalid
+     * @throws RuntimeException         for unexpected backend errors
+     */
+    default void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO primaryStorage) {
         // keep compatibility: by default ignore the extra arg and call legacy signature
         activeIscsiVolume(clientIqn, vol, shareable);
     }

若项目已采用可空性注解(如 @Nullable/@NotNull),也请同步标注以避免 NPE 争议。

Committable suggestion skipped: line range outside the PR's diff.


🛠️ Refactor suggestion

⚠️ Potential issue

新增参数导致接口破坏性变更,建议恢复旧签名并以 default 方法做过渡

此改动会在 5.3.46 稳定分支上直接破坏所有实现类与调用方(接口方法签名变化)。按“向后兼容原则”,建议保留旧方法签名(标记 @deprecated),并将新签名实现为 default,内部委托旧方法,给下游留出迁移窗口。同时将参数名由含糊的 vo 改为更语义化的 primaryStorage。示例修复:

-    void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO vo);
+    /**
+     * @deprecated Use {@link #activeIscsiVolume(String, BaseVolumeInfo, boolean, PrimaryStorageVO)}.
+     */
+    @Deprecated
+    void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable);
+
+    default void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO primaryStorage) {
+        // keep compatibility: by default ignore the extra arg and call legacy signature
+        activeIscsiVolume(clientIqn, vol, shareable);
+    }

说明:

  • 不改变现有实现类即可通过编译;新调用方可使用新签名;逐步迁移后再移除旧方法。
  • 参数命名采用完整语义,符合命名规范。
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO vo);
/**
* @deprecated Use {@link #activeIscsiVolume(String, BaseVolumeInfo, boolean, PrimaryStorageVO)}.
*/
@Deprecated
void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable);
default void activeIscsiVolume(String clientIqn, BaseVolumeInfo vol, boolean shareable, PrimaryStorageVO primaryStorage) {
// keep compatibility: by default ignore the extra arg and call legacy signature
activeIscsiVolume(clientIqn, vol, shareable);
}
🤖 Prompt for AI Agents
In
storage/src/main/java/org/zstack/storage/addon/primary/BlockExternalPrimaryStorageFactory.java
around line 14, the interface method signature was changed to add a new
parameter which breaks existing implementations; restore the original method
signature (mark it @Deprecated) and add a new default method with the new
signature that delegates to the old deprecated method, and use a clearer
parameter name (primaryStorage instead of vo) in the new method; this preserves
backward compatibility so existing implementors compile, allows new callers to
use the new signature, and gives downstreams time to migrate before removing the
deprecated method.

Resolves: ZSTAC-77315

Change-Id: I6670626f746f6e617466737965776f756a766774
@MatheMatrix MatheMatrix force-pushed the sync/jin.shen/test-77315@@2 branch from cf59c62 to 6d1d1a2 Compare September 26, 2025 05:56
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java (1)

90-103: 建议:为缺省端口的 iSCSI URI 补上默认值

URI#getPort() 在 URI 未显式给出端口时会返回 -1。当前直接写入 IscsiRemoteTarget,后续 getResourceURI() 会生成 iscsi://host:-1/... 这样的非法路径,容易让调用方误用。建议在解析阶段就补上 iSCSI 的默认端口 3260,或至少在缺省时显式失败,避免生成错误数据。

-            target.setPort(uri.getPort());
+            int port = uri.getPort() == -1 ? 3260 : uri.getPort();
+            target.setPort(port);
📜 Review details

Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf59c62 and 6d1d1a2.

📒 Files selected for processing (2)
  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java (2 hunks)
  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.*

⚙️ CodeRabbit configuration file

**/*.*: - 代码里不应当有有中文,包括报错、注释等都应当使用正确的、无拼写错误的英文来写

Files:

  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java
  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: ## 1. API 设计要求

  • API 命名:
    • API 名称必须唯一,不能重复。
    • API 消息类需要继承 APIMessage;其返回类必须继承 APIReplyAPIEvent,并在注释中用 @RestResponse 进行标注。
    • API 消息上必须添加注解 @RestRequest,并满足如下规范:
      • path:
        • 针对资源使用复数形式。
        • 当 path 中引用消息类变量时,使用 {variableName} 格式。
      • HTTP 方法对应:
        • 查询操作 → HttpMethod.GET
        • 更新操作 → HttpMethod.PUT
        • 创建操作 → HttpMethod.POST
        • 删除操作 → HttpMethod.DELETE
    • API 类需要实现 __example__ 方法以便生成 API 文档,并确保生成对应的 Groovy API Template 与 API Markdown 文件。

2. 命名与格式规范

  • 类名:

    • 使用 UpperCamelCase 风格。
    • 特殊情况:
      • VO/AO/EO 类型类除外。
      • 抽象类采用 AbstractBase 前缀/后缀。
      • 异常类应以 Exception 结尾。
      • 测试类需要以 TestCase 结尾。
  • 方法名、参数名、成员变量和局部变量:

    • 使用 lowerCamelCase 风格。
  • 常量命名:

    • 全部大写,使用下划线分隔单词。
    • 要求表达清楚,避免使用含糊或不准确的名称。
  • 包名:

    • 统一使用小写,使用点分隔符,每个部分应是一个具有自然语义的英文单词(参考 Spring 框架的结构)。
  • 命名细节:

    • 避免在父子类或同一代码块中出现相同名字的成员或局部变量,防止混淆。
    • 命名缩写:
      • 不允许使用不必要的缩写,如:AbsSchedulerJobcondiFu 等。应使用完整单词提升可读性。

3. 编写自解释代码

  • 意图表达:

    • 避免使用布尔型参数造成含义不明确。例如:
      • 对于 stopAgent(boolean ignoreError),建议拆分为不同函数(如 stopAgentIgnoreError()),或使用枚举表达操作类型。
    • 命名应尽量用完整的单词组合表达意图,并在名称中体现数据类型或用途(例如在常量与变量名称中,将类型词放在末尾)。
    • 避免使用魔法值(Magic Value):
      • 直接使用未经定义的数值或字符串(如 if (status == 5))应替换为枚举或常量。

      • 示例:

      • // 错误示例:魔法值

      • if (user.getStatus() == 5) { ... }

      • // 正确示例:常量或枚举

      • public static final int STATUS_ACTIVE = 5;

      • if (user.getStatus() == STATUS_ACTIVE) { ... }

      • // 或使用枚举

      • enum UserStatus { ACTIVE, INACTIVE }

  • 注释:

    • 代码应尽量做到自解释,对少于两行的说明可以直接写在代码中。
    • 对于较长的注释,需要仔细校对并随代码更新,确保内容正确。
    • 接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。

4. 流程控制和结构优化

  • if...else 的使用:

    • 应尽量减少 if...else 结构的使用,建议:
      • 限制嵌套层级最多为两层,且内层不应再出现 else 分支。
      • 尽早返回(Early Return),将条件判断中的处理逻辑提前结束或抽成独立方法。
      • 使用 Java Stream 或 Lambda 表达式代替冗长的循环与条件判断。
  • 条件判断:

    • if 条件表达不宜过长或过于复杂,必要时可以将条件抽成 boolean 变量描述。
  • 代码块长度:
    ...

Files:

  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java
  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java

Comment on lines 751 to 759
private void deactivateIscsi(String installPath, HostInventory h) {
String iqn = IscsiUtils.getHostInitiatorName(h.getUuid());
if (h.getHypervisorType().equals("baremetal2")) {
String[] parts = installPath.split("/");
String volumeUuid = parts[parts.length - 1];
VolumeVO volume = dbf.findByUuid(volumeUuid, VolumeVO.class);
iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

严重:baremetal2 卷卸载时会因路径解析错误触发 NPE

installPath 的最后一段是 diskIdType_diskId(例如 wwn_...),不是卷的 UUID。这里用它去 dbf.findByUuid 必然返回 null,进而在 volume.getVmInstanceUuid() 处抛出 NPE,导致 baremetal2 场景下的卸载流程直接失败。建议复用刚新增的 IscsiRemoteTarget.fromUri 来解析路径,并通过 diskId(WWN)反查 VolumeVO,同时对查不到或缺少 VM UUID 的情况做出显式处理。

-        if (h.getHypervisorType().equals("baremetal2")) {
-            String[] parts = installPath.split("/");
-            String volumeUuid = parts[parts.length - 1];
-            VolumeVO volume = dbf.findByUuid(volumeUuid, VolumeVO.class);
-            iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
+        if (h.getHypervisorType().equals("baremetal2")) {
+            IscsiRemoteTarget target = IscsiRemoteTarget.fromUri(installPath);
+            if (target == null || target.getDiskId() == null) {
+                throw new RuntimeException(String.format("invalid iscsi install path[%s]", installPath));
+            }
+            VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.wwn, target.getDiskId()).find();
+            if (volume == null || volume.getVmInstanceUuid() == null) {
+                throw new RuntimeException(String.format("cannot resolve vm uuid from iscsi install path[%s]", installPath));
+            }
+            iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void deactivateIscsi(String installPath, HostInventory h) {
String iqn = IscsiUtils.getHostInitiatorName(h.getUuid());
if (h.getHypervisorType().equals("baremetal2")) {
String[] parts = installPath.split("/");
String volumeUuid = parts[parts.length - 1];
VolumeVO volume = dbf.findByUuid(volumeUuid, VolumeVO.class);
iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
}
private void deactivateIscsi(String installPath, HostInventory h) {
String iqn = IscsiUtils.getHostInitiatorName(h.getUuid());
if (h.getHypervisorType().equals("baremetal2")) {
IscsiRemoteTarget target = IscsiRemoteTarget.fromUri(installPath);
if (target == null || target.getDiskId() == null) {
throw new RuntimeException(String.format("invalid iscsi install path[%s]", installPath));
}
VolumeVO volume = Q.New(VolumeVO.class)
.eq(VolumeVO_.wwn, target.getDiskId())
.find();
if (volume == null || volume.getVmInstanceUuid() == null) {
throw new RuntimeException(String.format("cannot resolve vm uuid from iscsi install path[%s]", installPath));
}
iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
}
🤖 Prompt for AI Agents
In plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
around lines 752 to 759, the code incorrectly treats the last segment of
installPath as a volume UUID causing dbf.findByUuid to return null and trigger
an NPE; replace this logic to parse installPath with IscsiRemoteTarget.fromUri
to extract the diskId (e.g. WWN), then query VolumeVO by that diskId (not by the
path segment) and handle both cases where no VolumeVO is found or the VolumeVO
lacks a vmInstanceUuid by logging an error and aborting the deactivate flow (or
returning early) to avoid NPE; keep existing iqn fallback behavior for
non-baremetal2 hosts.

shenjin added 2 commits September 26, 2025 14:47
Resolves: ZSTAC-77315

Change-Id: I71786673736d6473707a6d616c716c7474616c76
Resolves: ZSTAC-1111

Change-Id: I79646c6a747a6471676c6861676f787479716765
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d1d1a2 and 08a1e3c.

📒 Files selected for processing (2)
  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java (2 hunks)
  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java
🧰 Additional context used
📓 Path-based instructions (2)
**/*.*

⚙️ CodeRabbit configuration file

**/*.*: - 代码里不应当有有中文,包括报错、注释等都应当使用正确的、无拼写错误的英文来写

Files:

  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: ## 1. API 设计要求

  • API 命名:
    • API 名称必须唯一,不能重复。
    • API 消息类需要继承 APIMessage;其返回类必须继承 APIReplyAPIEvent,并在注释中用 @RestResponse 进行标注。
    • API 消息上必须添加注解 @RestRequest,并满足如下规范:
      • path:
        • 针对资源使用复数形式。
        • 当 path 中引用消息类变量时,使用 {variableName} 格式。
      • HTTP 方法对应:
        • 查询操作 → HttpMethod.GET
        • 更新操作 → HttpMethod.PUT
        • 创建操作 → HttpMethod.POST
        • 删除操作 → HttpMethod.DELETE
    • API 类需要实现 __example__ 方法以便生成 API 文档,并确保生成对应的 Groovy API Template 与 API Markdown 文件。

2. 命名与格式规范

  • 类名:

    • 使用 UpperCamelCase 风格。
    • 特殊情况:
      • VO/AO/EO 类型类除外。
      • 抽象类采用 AbstractBase 前缀/后缀。
      • 异常类应以 Exception 结尾。
      • 测试类需要以 TestCase 结尾。
  • 方法名、参数名、成员变量和局部变量:

    • 使用 lowerCamelCase 风格。
  • 常量命名:

    • 全部大写,使用下划线分隔单词。
    • 要求表达清楚,避免使用含糊或不准确的名称。
  • 包名:

    • 统一使用小写,使用点分隔符,每个部分应是一个具有自然语义的英文单词(参考 Spring 框架的结构)。
  • 命名细节:

    • 避免在父子类或同一代码块中出现相同名字的成员或局部变量,防止混淆。
    • 命名缩写:
      • 不允许使用不必要的缩写,如:AbsSchedulerJobcondiFu 等。应使用完整单词提升可读性。

3. 编写自解释代码

  • 意图表达:

    • 避免使用布尔型参数造成含义不明确。例如:
      • 对于 stopAgent(boolean ignoreError),建议拆分为不同函数(如 stopAgentIgnoreError()),或使用枚举表达操作类型。
    • 命名应尽量用完整的单词组合表达意图,并在名称中体现数据类型或用途(例如在常量与变量名称中,将类型词放在末尾)。
    • 避免使用魔法值(Magic Value):
      • 直接使用未经定义的数值或字符串(如 if (status == 5))应替换为枚举或常量。

      • 示例:

      • // 错误示例:魔法值

      • if (user.getStatus() == 5) { ... }

      • // 正确示例:常量或枚举

      • public static final int STATUS_ACTIVE = 5;

      • if (user.getStatus() == STATUS_ACTIVE) { ... }

      • // 或使用枚举

      • enum UserStatus { ACTIVE, INACTIVE }

  • 注释:

    • 代码应尽量做到自解释,对少于两行的说明可以直接写在代码中。
    • 对于较长的注释,需要仔细校对并随代码更新,确保内容正确。
    • 接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。

4. 流程控制和结构优化

  • if...else 的使用:

    • 应尽量减少 if...else 结构的使用,建议:
      • 限制嵌套层级最多为两层,且内层不应再出现 else 分支。
      • 尽早返回(Early Return),将条件判断中的处理逻辑提前结束或抽成独立方法。
      • 使用 Java Stream 或 Lambda 表达式代替冗长的循环与条件判断。
  • 条件判断:

    • if 条件表达不宜过长或过于复杂,必要时可以将条件抽成 boolean 变量描述。
  • 代码块长度:
    ...

Files:

  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
🧠 Learnings (1)
📚 Learning: 2025-08-25T03:52:37.301Z
Learnt from: ZStack-Robot
PR: MatheMatrix/zstack#2504
File: storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java:775-819
Timestamp: 2025-08-25T03:52:37.301Z
Learning: 在ZStack项目的VolumeSnapshotTreeBase类中,当通过dbf.findByUuid()方法获取VolumeVO对象时,需要进行null检查,因为该方法可能在找不到对应记录时返回null,直接使用可能导致NullPointerException。

Applied to files:

  • plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java
🧬 Code graph analysis (1)
plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java (1)
plugin/iscsi/src/main/java/org/zstack/iscsi/IscsiUtils.java (1)
  • IscsiUtils (18-56)
🔇 Additional comments (1)
plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java (1)

756-763: baremetal2 卷卸载仍然错误解析 installPath

installPath 的最后一段是 diskIdType_diskId,不是卷 UUID。继续用它去 dbf.findByUuid 会得到 null 并在 volume.getVmInstanceUuid() 处抛 NPE,baremetal2 的卸载流程仍然会失败。请改用刚新增的 IscsiRemoteTarget.fromUri 抽取 WWN,再按 WWN 反查卷并做好缺失校验。示例:

         if (h.getHypervisorType().equals("baremetal2")) {
-            String[] parts = installPath.split("/");
-            String volumeUuid = parts[parts.length - 1];
-            VolumeVO volume = dbf.findByUuid(volumeUuid, VolumeVO.class);
-            iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
+            IscsiRemoteTarget target = IscsiRemoteTarget.fromUri(installPath);
+            if (target == null || StringUtils.isEmpty(target.getDiskId())) {
+                throw new OperationFailureException(operr("invalid iscsi install path[%s]", installPath));
+            }
+            VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.wwn, target.getDiskId()).find();
+            if (volume == null || StringUtils.isEmpty(volume.getVmInstanceUuid())) {
+                throw new OperationFailureException(operr("cannot resolve vm uuid from iscsi install path[%s]", installPath));
+            }
+            iqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
         } else {

Comment on lines 239 to +243
private synchronized ActiveVolumeTO activeIscsiVolume(HostInventory h, BaseVolumeInfo vol, boolean shareable) {
String clientIqn = IscsiUtils.getHostInitiatorName(h.getUuid());
String clientIqn;
if (h.getHypervisorType().equals("baremetal2")) {
VolumeVO volume = dbf.findByUuid(vol.getUuid(), VolumeVO.class);
clientIqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
} else {
clientIqn = IscsiUtils.getHostInitiatorName(h.getUuid());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

baremetal2 卷激活缺少 Volume 空指针校验

dbf.findByUuid() 找不到记录会返回 null,目前 baremetal2 分支立刻访问 volume.getVmInstanceUuid(),会触发 NPE 并终止卷激活流程。请先校验 Volume 是否存在且挂载了 VM,再生成 IQN。修正示例:

         if (h.getHypervisorType().equals("baremetal2")) {
-            VolumeVO volume = dbf.findByUuid(vol.getUuid(), VolumeVO.class);
-            clientIqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
+            VolumeVO volume = dbf.findByUuid(vol.getUuid(), VolumeVO.class);
+            if (volume == null || StringUtils.isEmpty(volume.getVmInstanceUuid())) {
+                throw new OperationFailureException(operr("cannot resolve vm uuid from volume[%s] for host[%s]", vol.getUuid(), h.getUuid()));
+            }
+            clientIqn = String.format("iqn.2015-01.io.zstack:initiator.instance.%s", volume.getVmInstanceUuid());
         } else {

Based on learnings

Resolves: ZSTAC-1111

Change-Id: I7763786c70636b6d636f6b776e73787a67687565
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08a1e3c and 02a5815.

📒 Files selected for processing (1)
  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.*

⚙️ CodeRabbit configuration file

**/*.*: - 代码里不应当有有中文,包括报错、注释等都应当使用正确的、无拼写错误的英文来写

Files:

  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: ## 1. API 设计要求

  • API 命名:
    • API 名称必须唯一,不能重复。
    • API 消息类需要继承 APIMessage;其返回类必须继承 APIReplyAPIEvent,并在注释中用 @RestResponse 进行标注。
    • API 消息上必须添加注解 @RestRequest,并满足如下规范:
      • path:
        • 针对资源使用复数形式。
        • 当 path 中引用消息类变量时,使用 {variableName} 格式。
      • HTTP 方法对应:
        • 查询操作 → HttpMethod.GET
        • 更新操作 → HttpMethod.PUT
        • 创建操作 → HttpMethod.POST
        • 删除操作 → HttpMethod.DELETE
    • API 类需要实现 __example__ 方法以便生成 API 文档,并确保生成对应的 Groovy API Template 与 API Markdown 文件。

2. 命名与格式规范

  • 类名:

    • 使用 UpperCamelCase 风格。
    • 特殊情况:
      • VO/AO/EO 类型类除外。
      • 抽象类采用 AbstractBase 前缀/后缀。
      • 异常类应以 Exception 结尾。
      • 测试类需要以 TestCase 结尾。
  • 方法名、参数名、成员变量和局部变量:

    • 使用 lowerCamelCase 风格。
  • 常量命名:

    • 全部大写,使用下划线分隔单词。
    • 要求表达清楚,避免使用含糊或不准确的名称。
  • 包名:

    • 统一使用小写,使用点分隔符,每个部分应是一个具有自然语义的英文单词(参考 Spring 框架的结构)。
  • 命名细节:

    • 避免在父子类或同一代码块中出现相同名字的成员或局部变量,防止混淆。
    • 命名缩写:
      • 不允许使用不必要的缩写,如:AbsSchedulerJobcondiFu 等。应使用完整单词提升可读性。

3. 编写自解释代码

  • 意图表达:

    • 避免使用布尔型参数造成含义不明确。例如:
      • 对于 stopAgent(boolean ignoreError),建议拆分为不同函数(如 stopAgentIgnoreError()),或使用枚举表达操作类型。
    • 命名应尽量用完整的单词组合表达意图,并在名称中体现数据类型或用途(例如在常量与变量名称中,将类型词放在末尾)。
    • 避免使用魔法值(Magic Value):
      • 直接使用未经定义的数值或字符串(如 if (status == 5))应替换为枚举或常量。

      • 示例:

      • // 错误示例:魔法值

      • if (user.getStatus() == 5) { ... }

      • // 正确示例:常量或枚举

      • public static final int STATUS_ACTIVE = 5;

      • if (user.getStatus() == STATUS_ACTIVE) { ... }

      • // 或使用枚举

      • enum UserStatus { ACTIVE, INACTIVE }

  • 注释:

    • 代码应尽量做到自解释,对少于两行的说明可以直接写在代码中。
    • 对于较长的注释,需要仔细校对并随代码更新,确保内容正确。
    • 接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。

4. 流程控制和结构优化

  • if...else 的使用:

    • 应尽量减少 if...else 结构的使用,建议:
      • 限制嵌套层级最多为两层,且内层不应再出现 else 分支。
      • 尽早返回(Early Return),将条件判断中的处理逻辑提前结束或抽成独立方法。
      • 使用 Java Stream 或 Lambda 表达式代替冗长的循环与条件判断。
  • 条件判断:

    • if 条件表达不宜过长或过于复杂,必要时可以将条件抽成 boolean 变量描述。
  • 代码块长度:
    ...

Files:

  • header/src/main/java/org/zstack/header/storage/addon/IscsiRemoteTarget.java

Comment on lines +91 to +105
String[] serverHostNames = uri.getAuthority().split(":")[0].split(",");
target.setIp(serverHostNames[0]);
target.setPort(Integer.parseInt(uri.getAuthority().split(":")[1]));

// parse: /{iqn}/{diskIdType}_{diskId}
String path = uri.getPath();
if (path != null && path.startsWith("/")) {
String[] pathParts = path.substring(1).split("/");
if (pathParts.length >= 2) {
target.setIqn(pathParts[0]);
String[] diskParts = pathParts[1].split("_", 2);
if (diskParts.length == 2) {
target.setDiskIdType(diskParts[0]);
target.setDiskId(diskParts[1]);
} else {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

修正 IPv6 与缺失端口的 URI 解析漏洞

当前依赖 uri.getAuthority().split(":") 同时截取主机与端口。在合法的 IPv6 形式(例:iscsi://[2001:db8::1]:3260/...)下该表达式会把地址切成 "[2001", "db8::1]", "3260",导致写入的 IP 变成 "[2001",端口解析也会抛 NumberFormatException。遇到没有显式端口的 URI 时同样会触发 ArrayIndexOutOfBoundsException,使得原本可接受的输入直接判为失败。建议改用 URI#getHost()URI#getPort() 提供的结构化解析,并在缺失信息时给出清晰的校验逻辑。如下修订可避免该问题:

-            String[] serverHostNames = uri.getAuthority().split(":")[0].split(",");
-            target.setIp(serverHostNames[0]);
-            target.setPort(Integer.parseInt(uri.getAuthority().split(":")[1]));
+            String authority = uri.getAuthority();
+            if (authority == null) {
+                logger.info("Missing authority in URI: " + uriString);
+                return null;
+            }
+
+            String hostSegment = uri.getHost();
+            if (hostSegment == null) {
+                hostSegment = authority;
+                if (authority.contains("@")) {
+                    hostSegment = authority.substring(authority.indexOf('@') + 1);
+                }
+                int lastColon = hostSegment.lastIndexOf(':');
+                if (lastColon >= 0) {
+                    hostSegment = hostSegment.substring(0, lastColon);
+                }
+            }
+
+            if (hostSegment == null || hostSegment.isEmpty()) {
+                logger.info("Missing host in URI authority: " + authority);
+                return null;
+            }
+
+            String[] serverHostNames = hostSegment.split(",");
+            target.setIp(serverHostNames[0]);
+
+            int parsedPort = uri.getPort();
+            if (parsedPort < 0) {
+                logger.info("Missing port in URI authority: " + authority);
+                return null;
+            }
+            target.setPort(parsedPort);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String[] serverHostNames = uri.getAuthority().split(":")[0].split(",");
target.setIp(serverHostNames[0]);
target.setPort(Integer.parseInt(uri.getAuthority().split(":")[1]));
// parse: /{iqn}/{diskIdType}_{diskId}
String path = uri.getPath();
if (path != null && path.startsWith("/")) {
String[] pathParts = path.substring(1).split("/");
if (pathParts.length >= 2) {
target.setIqn(pathParts[0]);
String[] diskParts = pathParts[1].split("_", 2);
if (diskParts.length == 2) {
target.setDiskIdType(diskParts[0]);
target.setDiskId(diskParts[1]);
} else {
String authority = uri.getAuthority();
if (authority == null) {
logger.info("Missing authority in URI: " + uriString);
return null;
}
String hostSegment = uri.getHost();
if (hostSegment == null) {
// fallback to manual parsing of authority (e.g. userinfo@host:port)
hostSegment = authority;
if (authority.contains("@")) {
hostSegment = authority.substring(authority.indexOf('@') + 1);
}
int lastColon = hostSegment.lastIndexOf(':');
if (lastColon >= 0) {
hostSegment = hostSegment.substring(0, lastColon);
}
}
if (hostSegment.isEmpty()) {
logger.info("Missing host in URI authority: " + authority);
return null;
}
String[] serverHostNames = hostSegment.split(",");
target.setIp(serverHostNames[0]);
int parsedPort = uri.getPort();
if (parsedPort < 0) {
logger.info("Missing port in URI authority: " + authority);
return null;
}
target.setPort(parsedPort);
// parse: /{iqn}/{diskIdType}_{diskId}
String path = uri.getPath();
if (path != null && path.startsWith("/")) {
String[] pathParts = path.substring(1).split("/");
if (pathParts.length >= 2) {
target.setIqn(pathParts[0]);
String[] diskParts = pathParts[1].split("_", 2);
if (diskParts.length == 2) {
target.setDiskIdType(diskParts[0]);
target.setDiskId(diskParts[1]);
} else {
// …
}
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant