diff --git a/docs/design/SUG-2668-policy-rule-scheduler.md b/docs/design/SUG-2668-policy-rule-scheduler.md
new file mode 100644
index 00000000000..b3f97b47157
--- /dev/null
+++ b/docs/design/SUG-2668-policy-rule-scheduler.md
@@ -0,0 +1,1007 @@
+---
+summary: "支持安全组和 VPC 防火墙规则集按计划定时生效。"
+version: "5.5.38"
+jira_key: "SUG-2668"
+jira: "http://jira.zstack.io/browse/SUG-2668"
+status: "Draft"
+last_updated: "2026-07-27"
+---
+
+# Func Spec: 安全组/防火墙定时生效
+
+> `version` 固定为 `5.5.38`,后续修改本 spec 时不得变更。
+
+## 1. 背景与范围
+
+本需求来源于 [SUG-2668](http://jira.zstack.io/browse/SUG-2668)。用户需要为安全组或 VPC 防火墙规则集设置定时生效计划,使规则在指定时间内自动生效或失效,避免在上下班、临时开放等场景中手工启停规则。
+
+定时计划支持一次性生效和每周重复生效。每周计划可同时限定固定日期范围、星期和每天时段。定时计划不得改变规则原有启停状态,未设置计划的安全组和防火墙规则集保持现有行为。
+
+## 2. 功能范围
+
+### 2.1 功能点
+
+| 编号 | 功能点 | 说明 |
+|---|---|---|
+| F-01 | 定时计划管理 |
|
+| F-02 | 时间基准 | UTC 和本地时间;本地时间保存平台 IANA 时区 ID。 |
+| F-03 | 一次性计划 | 分钟级绝对生效区间。 |
+| F-04 | 每周计划 | - 固定日期范围
- 星期
- 每日生效时段;`00:00—00:00` 表示全天
|
+| F-05 | 绑定范围 | 仅支持安全组和 VPC 防火墙规则集;单个资源只能绑定一个计划。 |
+| F-06 | 周期检查 | 每分钟扫描已绑定计划的资源,仅刷新结果变化的资源。 |
+| F-07 | 安全组刷新 | 生效结果变化时,按 Host 下发完整规则。 |
+| F-08 | VPC 防火墙刷新 | 生效结果变化时,按关联防火墙批量替换规则集配置。 |
+| F-09 | 原有启停兼容 | - Disabled 规则始终不生效
- 不修改规则状态和原有启停 API 行为
|
+| F-10 | 双 MN 和并发 | - 周期检查在双 MN 中只执行一次
- 同一资源的周期刷新与用户操作串行执行
|
+| F-11 | 升级兼容 | `schedulerUuid` 为空的存量资源保持原行为。 |
+
+### 2.2 非本期目标
+
+- 不使用 iptables `time` 扩展。
+- 不在 Host 或 VRouter 保存或计算定时计划。
+- 不改造现有单规则/单操作 API。
+- 不允许将定时计划绑定到安全组规则、VPC 防火墙或防火墙规则。
+- 不支持一天多个时段、跨天时段、节假日、按月和 Cron 表达式。
+- 不保存规则当前生效状态、下次切换时间和执行历史。
+
+## 3. 后端方案设计
+
+### 3.1 总体方案
+
+```text
+┌──────────────────────────────────────── 用户层 ────────────────────────────────────────┐
+│ 用户 ── UI / CLI / SDK ──> Cloud API │
+└────────────────────────────────────────────┬───────────────────────────────────────────┘
+ │
+┌────────────────────────────────────────────▼───────────────────────────────────────────┐
+│ 控制面(MN) │
+│ │
+│ 安全组操作 定时计划管理、绑定/解绑 防火墙规则集操作 │
+│ │ │ │ │
+│ ▼ ▼ ▼ │
+│ ┌────────────────────┐ 计划查询 ┌────────────────────┐ 计划查询 ┌────────────────────┐ │
+│ │ 安全组服务 │───────────>│ 定时计划服务 │<──────────│ VPC 防火墙服务 │ │
+│ │ 安全组及其规则 │<───────────│ 定时计划与时间判定 │──────────>│ 规则集及其规则 │ │
+│ └──────────┬─────────┘ 资源刷新 └─────────▲──────────┘ 资源刷新 └─────────┬──────────┘ │
+│ ▲ │ ▲ │
+│ VM / Host 生命周期 每分钟周期检查 VRouter / HA 生命周期 │
+└─────────────┼──────────────────────────────────────────────────────────────┼──────────────┘
+ │ │
+ ▼ ▼
+┌──────────────────────────────────────── 数据面 ────────────────────────────────────────┐
+│ Host 安全组规则(iptables) VRouter 防火墙规则(iptables) │
+└────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 3.2 定时计划时间规则
+
+```text
+Once
+
+ startDate startTime endDate endTime
+ ●════════════════ 规则生效 ═════════════════════○
+ └──────────────── 左闭右开 ─────────────────────┘
+
+Weekly
+
+ startDate endDate
+ ├────────────────── 固定日期范围 ─────────────────────┤
+ │ 周一 startTime ●════════════════════○ endTime │
+ │ 周二 startTime ●════════════════════○ endTime │
+ │ ... │
+ │ 周五 startTime ●════════════════════○ endTime │
+ │ 全天 00:00 ═════════ 全天约定 ═════════ 00:00 │
+ └──────────────────────────────────────────────────────┘
+```
+
+- 日期格式为 `yyyy-MM-dd`,时间格式为 `HH:mm`;API 不接受秒,数据库中的秒固定为 `00`。
+- `Once` 的起止日期和时间组成绝对区间,开始分钟包含,结束分钟不包含。
+- `Weekly` 同时满足以下三个条件时生效:当前日期位于 `startDate—endDate` 固定日期范围内、当前星期位于 `weekDays`、当前时间位于 `startTime—endTime` 每日时段。
+- `Weekly` 的固定日期范围包含首尾且允许单日;`weekDays` 使用 1—7 表示星期一至星期日;`00:00—00:00` 表示所选星期全天生效,其他时段必须为同一天内的左闭右开正区间。全天约定不表示跨天时段。
+- 本地时间在创建时固化为平台当前 IANA 时区 ID,例如 `Asia/Shanghai`,后续不受平台时区配置变化影响。
+- 本地时间判定使用 `ZonedDateTime` 结合计划时区,含 DST 的时区以归一化结果为准。
+
+### 3.3 绑定关系和生效规则
+
+- 每个安全组或 VPC 防火墙规则集最多绑定一个计划,同一计划可以绑定多个资源。
+- 原有状态为 `Disabled` 的规则始终不生效;其他规则在资源未绑定计划时保持原行为,绑定后仅在计划内生效。
+- 计划只影响下发结果,不修改安全组规则的 `state`、防火墙规则的 `state/isApplied/expired` 或规则集发布状态。
+
+### 3.4 周期检查任务
+
+```text
+MN-1 ThreadFacade MN-2 ThreadFacade
+ │ 每 60 秒,对齐整分钟 │ 每 60 秒,对齐整分钟
+ ▼ ▼
+isManagedByUs(固定任务标识) isManagedByUs(固定任务标识)
+ │ true │ false
+ ▼ └─> 跳过
+ 上次成功检查 T1 ───────────────── 当前分钟 T2
+ │
+ ├─> 按安全组分组计算 T1/T2
+ │ └─> 状态变化 ──> ChangeSecurityGroupSchedulerMsg(REFRESH)
+ │
+ └─> 按防火墙规则集分组计算 T1/T2
+ └─> 状态变化 ──> ChangeVpcFirewallRuleSetSchedulerMsg(REFRESH)
+```
+
+- `PolicyRuleSchedulerScanTask` 不使用通用 Scheduler Job/Trigger,避免生成分钟级 `SchedulerJobHistoryVO`;T1 和失败待重试资源仅保存在负责 MN 内存中。
+- 用 `ThreadFacade.submitPeriodicTask` 以 60 秒为周期运行,通过初始延迟对齐到下一整分钟。
+- T1/T2 取实际 wall-clock 分钟(按各计划时区归一到分钟),按两端点比较状态变化,不逐分钟回放。
+- 扫描任务 `run()` 最外层捕获全部异常并记录告警(未捕获异常会使 `scheduleAtFixedRate` 永久取消任务),本轮不更新 T1。
+- 失去任务归属时清空本地状态;首次取得任务归属时刷新全部安全组和 VPC 防火墙规则集。
+- 扫描计算或消息投递异常时不更新 T1;资源刷新失败加入待重试集合。
+- Update、Delete、Attach 和 Detach 立即刷新受影响资源,不等待周期任务;新建计划不触发刷新。
+- 常规轮次仅扫描已绑定资源,仅刷新结果变化或等待重试的资源。
+
+### 3.5 安全组和防火墙处理
+
+| 处理项 | 安全组 | VPC 防火墙规则集 |
+|---|---|---|
+| 查询数据 | 安全组、规则和关联 VM 网卡 | 规则集当前已发布规则和 `VpcFirewallRuleSetL3RefVO` |
+| 原有状态 | `Disabled` 规则不下发 | `Disabled` 规则保持禁用;只处理 `isApplied=true` 的当前版本 |
+| 下发单位 | 每个 Host 一份完整规则 | 每个关联防火墙一条 `ApplyRuleSetChangesCmd` |
+| 空结果 | 保留安全组 UUID 并下发空规则,清理 Host 旧规则 | 规则集未挂载到任何防火墙时直接成功 |
+| 数据面 | `zstack-utility` 刷新 iptables 链 | `zstack-vyos` 批量替换规则集配置 |
+
+- 防火墙规则集以 `isApplied=true` 确定当前版本;已标记 `expired` 但尚未被正式 Apply 替换的旧规则仍属于当前版本。
+- `ApplyRuleSetChangesCmd.deleteRules` 携带当前版本,`newRules` 携带按计划调整后的同一版本。命令按 `VpcFirewallRuleSetL3RefVO` 关联的防火墙分组,并覆盖 HA 对端。
+- `VpcFirewallRuleBuilder` 统一生成规则 TO,供规则创建、规则启停、`APIApplyRuleSetChanges`、规则集挂载、VRouter 启动/重启、HA 同步和完整刷新调用。
+- 安全组规则修改、VM 迁移、Host 重连,以及 VRouter 启动、HA 同步、规则集挂载和完整配置刷新,均按执行时的计划结果生成规则。
+- 安全组因计划外导致 0 条有效规则时,只要仍有关联 VM 网卡就以空规则下发对应安全组,清理 Host 旧规则,不将其从 `HostRuleTO` 中丢弃。
+- 安全组 Host 失败仍写入 `SecurityGroupFailureHostVO`,不改变现有安全组 API 的成功语义。
+- 防火墙规则增删改、启停和规则集修改复用 `Firewall-RuleSet-${ruleSetUuid}` 队列,公开 API 不变。
+- `zstack-utility` 和 `zstack-vyos` 的重复刷新幂等。
+
+### 3.6 并发和双 MN
+
+```text
+Update/Attach/Detach ────────────────┐
+ ├─> PolicyRuleScheduler-${schedulerUuid}
+Delete ─> Cascade ─> Delete*Msg ─────┘ │
+ └─> 资源本地消息
+ │
+用户修改规则 ──────────────────────────────────────────────────────────────┤
+周期检查任务 ──> Change*SchedulerMsg(REFRESH) ──────────────────────────────┤
+ │
+ ┌──────────────────────────────────┴─────────────────────────────────┐
+ ▼ ▼
+ SecurityGroup-${securityGroupUuid} Firewall-RuleSet-${ruleSetUuid}
+
+MN-1:isManagedByUs = true ────────────────X
+ MN 成员变化后重新分配固定任务标识
+MN-2:isManagedByUs = false ─────────────────────> 下一整分钟首次全量刷新 ──> 周期检查
+```
+
+Attach/Detach 固定从计划队列调用资源消息;不同计划并发绑定同一资源时,后进入资源队列的请求重新读取绑定关系。
+
+## 4. 模块依赖
+
+时间策略位于新增的 `plugin-premium/policyRuleScheduler` 模块。
+
+```text
+plugin-premium/policyRuleScheduler
+ |- PolicyRuleSchedulerVO / API / Cascade
+ |- PolicyRuleSchedulerFacade
+ |- PolicyRuleSchedulerScanTask
+ |- PolicyRuleSchedulerSecurityGroupExtension
+ | implements
+ | └─ zstack/plugin/securityGroup
+ | └─ SecurityGroupRuleFilterExtensionPoint
+ |
+ └─ PolicyRuleSchedulerVpcFirewallExtension
+ implements
+ └─ plugin-premium/vpcFirewall
+ └─ VpcFirewallRuleStateExtensionPoint
+```
+
+| 模块 | 本期职责 |
+|---|---|
+| `plugin-premium/policyRuleScheduler` | 保存定时计划并提供 API、周期检查;`PolicyRuleSchedulerFacade` 负责可绑定性、当前生效状态和批量计算;实现两类规则扩展。 |
+| `zstack/plugin/securityGroup` | 增加 `schedulerUuid`、`ChangeSecurityGroupSchedulerMsg` 和规则过滤扩展点;`RuleCalculator` 在原有 `Enabled` 过滤后调用扩展。 |
+| `plugin-premium/vpcFirewall` | 增加 `schedulerUuid`、`ChangeVpcFirewallRuleSetSchedulerMsg`、规则状态扩展点和 `VpcFirewallRuleBuilder`,复用现有批量下发。 |
+| `zstack-utility` / `zstack-vyos` | 不解析定时计划,处理 MN 下发的最终规则并补充必要的幂等适配。 |
+
+`policyRuleScheduler` 依赖安全组和 VPC 防火墙模块;资源模块只依赖各自定义的扩展接口,不依赖计划表或时间计算实现。
+未注册扩展实现时,资源模块直接使用原规则结果。
+
+## 5. 数据库表设计
+
+```sql
+-- premium:安全策略定时计划
+CREATE TABLE IF NOT EXISTS `PolicyRuleSchedulerVO` (
+ `uuid` varchar(32) NOT NULL, -- 计划 UUID
+ `name` varchar(255) NOT NULL, -- 计划名称
+ `description` varchar(2048) DEFAULT NULL, -- 计划描述
+ `timeZone` varchar(64) NOT NULL, -- UTC 或 IANA 时区 ID,例如 Asia/Shanghai
+ `repeatType` varchar(32) NOT NULL, -- Once 或 Weekly
+ `startDate` date NOT NULL, -- Once 的开始日期或 Weekly 固定日期范围的开始日期
+ `endDate` date NOT NULL, -- Once 的结束日期或 Weekly 固定日期范围的结束日期
+ `startTime` time NOT NULL, -- Once 或 Weekly 的开始时间;Weekly 全天时为 00:00:00
+ `endTime` time NOT NULL, -- Once 或 Weekly 的结束时间;Weekly 全天时为 00:00:00
+ `weekDays` varchar(32) DEFAULT NULL, -- Weekly 使用,1 至 7 逗号分隔;Once 为空
+ `createDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', -- 创建时间
+ `lastOpDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
+ ON UPDATE CURRENT_TIMESTAMP, -- 最后修改时间
+ PRIMARY KEY (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- zstack:安全组的计划引用
+ALTER TABLE `SecurityGroupVO`
+ ADD COLUMN `schedulerUuid` varchar(32) DEFAULT NULL; -- 安全组使用的定时计划 UUID
+
+-- premium:VPC 防火墙规则集的计划引用
+ALTER TABLE `VpcFirewallRuleSetVO`
+ ADD COLUMN `schedulerUuid` varchar(32) DEFAULT NULL; -- 防火墙规则集使用的定时计划 UUID
+```
+
+```java
+@BaseResource
+@Entity
+@Table
+class PolicyRuleSchedulerVO extends ResourceVO implements OwnedByAccount {
+ @Transient
+ String accountUuid;
+}
+```
+
+安全组和防火墙规则集通过 `schedulerUuid` 引用计划,该挂载关系不增加关联表和数据库外键。`SecurityGroupVO.schedulerUuid` 落在开源 `zstack` 表,仅由 premium 填充;计划语义只在 premium 解析。
+
+`PolicyRuleSchedulerVO` 不保存当前生效状态或可绑定状态。两者都由计划内容、时区和当前分钟计算;绑定计划时,只有仍存在未来生效分钟的计划才允许绑定。
+
+删除账号时级联删除其计划。删除计划时级联清空安全组和防火墙规则集的 `schedulerUuid` 并刷新受影响资源;删除安全组或防火墙规则集不删除可复用计划。
+
+## 6. API 设计
+
+### 6.1 APICreatePolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler")
+@TagResourceType(PolicyRuleSchedulerVO.class)
+@RestRequest(path = "/policy-rule-schedulers", method = POST,
+ responseClass = APICreatePolicyRuleSchedulerEvent.class)
+class APICreatePolicyRuleSchedulerMsg extends APICreateMessage {
+ @APIParam(maxLength = 255) String name;
+ @APIParam(required = false, maxLength = 2048) String description;
+ @APIParam String timeZone; // UTC 或 IANA 时区 ID
+ @APIParam(validValues = {"Once", "Weekly"}) String repeatType;
+ @APIParam String startDate; // yyyy-MM-dd;Weekly 固定日期范围开始
+ @APIParam String endDate; // yyyy-MM-dd;Weekly 固定日期范围结束
+ @APIParam String startTime; // HH:mm;Weekly 全天时与 endTime 均为 00:00
+ @APIParam String endTime; // HH:mm
+ @APIParam(required = false) List weekDays;
+}
+```
+
+#### Validate
+
+- `timeZone` 必须为 `UTC` 或有效的 IANA 时区 ID;日期和时间分别使用 `yyyy-MM-dd`、`HH:mm`。
+- `Once`:`weekDays` 为空,按 `timeZone` 合成的绝对时间范围必须为正区间。
+- `Weekly`:`startDate` 不晚于 `endDate`;`weekDays` 非空、不重复且取值为 1—7;每日时段仅允许 `00:00—00:00`(全天)或同日正区间。
+
+#### Workflow
+
+```text
+APICreatePolicyRuleSchedulerMsg
+ |
+ └─ persist PolicyRuleSchedulerVO
+```
+
+### 6.2 APIUpdatePolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler")
+@RestRequest(path = "/policy-rule-schedulers/{uuid}/actions", method = PUT, isAction = true,
+ responseClass = APIUpdatePolicyRuleSchedulerEvent.class)
+class APIUpdatePolicyRuleSchedulerMsg extends APIMessage
+ implements PolicyRuleSchedulerMessage {
+ @APIParam(resourceType = PolicyRuleSchedulerVO.class, checkAccount = true, operationTarget = true)
+ String uuid;
+ @APIParam(maxLength = 255) String name;
+ @APIParam(required = false, maxLength = 2048) String description;
+ @APIParam String timeZone;
+ @APIParam(validValues = {"Once", "Weekly"}) String repeatType;
+ @APIParam String startDate;
+ @APIParam String endDate;
+ @APIParam String startTime; // Weekly 全天时与 endTime 均为 00:00
+ @APIParam String endTime;
+ @APIParam(required = false) List weekDays;
+
+ @Override
+ String getSchedulerUuid() { return uuid; }
+}
+```
+
+#### Validate
+
+- 请求全量覆盖计划(PUT 全量替换,非字段级 patch),不读取数据库原值补齐;必填字段缺失时失败,`description` 未提交时清空。
+- `Once` 要求 `weekDays` 为空并保存为 `NULL`;`Weekly` 必须提交非空 `weekDays`;全量替换后的时间表达按创建 API 校验,包括 `Weekly` 的全天约定。
+- 更新后可以没有未来生效分钟;已有绑定保留并立即刷新,新的 Attach 被拒绝。
+
+#### Workflow
+
+```text
+APIUpdatePolicyRuleSchedulerMsg
+ |
+ |- Flow["update-policy-rule-scheduler"]
+ |
+ |- [then]: Flow["update-scheduler"]
+ | |
+ | |- load current scheduler
+ | |- [time expression changed]: load referenced resources
+ | |- replace PolicyRuleSchedulerVO
+ | |
+ | └─ [rollback]: "restore-scheduler"
+ | |
+ | |- restore old PolicyRuleSchedulerVO
+ | └─ [time expression changed]: refresh referenced resources
+ |
+ └─ [then, time expression changed]: NoRollbackFlow["refresh-referenced-resources"]
+ |
+ |- ChangeSecurityGroupSchedulerMsg(securityGroupUuid, operation=REFRESH)
+ └─ ChangeVpcFirewallRuleSetSchedulerMsg(ruleSetUuid, operation=REFRESH)
+```
+
+只修改名称或描述时不刷新资源。恢复动作失败时保留原始错误并记录告警。
+
+### 6.3 APIDeletePolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler")
+@RestRequest(path = "/policy-rule-schedulers/{uuid}", method = DELETE,
+ responseClass = APIDeletePolicyRuleSchedulerEvent.class)
+class APIDeletePolicyRuleSchedulerMsg extends APIDeleteMessage {
+ @APIParam(resourceType = PolicyRuleSchedulerVO.class, checkAccount = true,
+ operationTarget = true, successIfResourceNotExisting = true)
+ String uuid;
+}
+```
+
+#### Validate
+
+- 被引用计划允许删除,由 Cascade 解绑。
+
+#### Workflow
+
+```text
+APIDeletePolicyRuleSchedulerMsg
+ |
+ └─ CascadeFacade
+ |
+ └─ PolicyRuleSchedulerCascadeExtension
+ |
+ └─ DeletePolicyRuleSchedulerMsg(schedulerUuid)
+ |
+ |- [then]: Flow["delete-scheduler"]
+ | |
+ | |- load referenced resources
+ | └─ SQLBatch
+ | |
+ | |- clear SecurityGroupVO.schedulerUuid
+ | |- clear VpcFirewallRuleSetVO.schedulerUuid
+ | └─ remove PolicyRuleSchedulerVO
+ |
+ └─ [then]: NoRollbackFlow["refresh-unbound-resources"]
+ |
+ |- ChangeSecurityGroupSchedulerMsg(securityGroupUuid, operation=REFRESH)
+ └─ ChangeVpcFirewallRuleSetSchedulerMsg(ruleSetUuid, operation=REFRESH)
+```
+
+清空引用和删除计划在同一个数据库事务中完成。资源刷新采用 best-effort,失败不恢复已删除计划。
+
+### 6.4 APIQueryPolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler", names = {"read"})
+@RestRequest(path = "/policy-rule-schedulers",
+ optionalPaths = {"/policy-rule-schedulers/{uuid}"}, method = GET,
+ responseClass = APIQueryPolicyRuleSchedulerReply.class)
+@AutoQuery(replyClass = APIQueryPolicyRuleSchedulerReply.class,
+ inventoryClass = PolicyRuleSchedulerInventory.class)
+class APIQueryPolicyRuleSchedulerMsg extends APIQueryMessage {}
+```
+
+#### Validate
+
+- 无。
+
+#### Workflow
+
+```text
+APIQueryPolicyRuleSchedulerMsg
+ |
+ |- AutoQuery["query-policy-rule-scheduler"]
+ |
+ |- Query PolicyRuleSchedulerVO
+```
+
+### 6.5 APIAttachPolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler")
+@RestRequest(path = "/policy-rule-schedulers/actions", method = PUT, isAction = true,
+ responseClass = APIAttachPolicyRuleSchedulerEvent.class)
+class APIAttachPolicyRuleSchedulerMsg extends APIMessage
+ implements PolicyRuleSchedulerMessage {
+ @APIParam(resourceType = PolicyRuleSchedulerVO.class, checkAccount = true)
+ String schedulerUuid;
+ @APIParam(validValues = {"SecurityGroup", "VpcFirewallRuleSet"})
+ String resourceType;
+ @APIParam(resourceType = ResourceVO.class, checkAccount = true, operationTarget = true)
+ String resourceUuid;
+
+ @Override
+ String getSchedulerUuid() { return schedulerUuid; }
+}
+```
+
+```text
+resourceType
+ |- SecurityGroup -> SecurityGroupVO
+ └─ VpcFirewallRuleSet -> VpcFirewallRuleSetVO
+```
+
+#### Validate
+
+- `resourceUuid` 必须存在且匹配 `resourceType`,不尝试推断其他类型。
+- 计划在校验时必须仍有未来生效分钟。
+- 目标已绑定时拒绝;更换计划需先解绑。
+
+#### Workflow
+
+```text
+APIAttachPolicyRuleSchedulerMsg
+ |
+ |- reload and validate latest scheduler
+ |
+ |- [resourceType = SecurityGroup]:
+ | ChangeSecurityGroupSchedulerMsg(
+ | securityGroupUuid, schedulerUuid, operation=ATTACH)
+ |
+ └─ [resourceType = VpcFirewallRuleSet]:
+ ChangeVpcFirewallRuleSetSchedulerMsg(
+ ruleSetUuid, schedulerUuid, operation=ATTACH)
+```
+
+### 6.6 APIDetachPolicyRuleScheduler
+
+#### API
+
+```java
+@Action(category = "policyRuleScheduler")
+@RestRequest(path = "/policy-rule-schedulers/actions", method = PUT, isAction = true,
+ responseClass = APIDetachPolicyRuleSchedulerEvent.class)
+class APIDetachPolicyRuleSchedulerMsg extends APIMessage
+ implements PolicyRuleSchedulerMessage {
+ @APIParam(resourceType = PolicyRuleSchedulerVO.class, checkAccount = true)
+ String schedulerUuid;
+ @APIParam(validValues = {"SecurityGroup", "VpcFirewallRuleSet"})
+ String resourceType;
+ @APIParam(resourceType = ResourceVO.class, checkAccount = true, operationTarget = true)
+ String resourceUuid;
+
+ @Override
+ String getSchedulerUuid() { return schedulerUuid; }
+}
+```
+
+#### Validate
+
+- `resourceUuid` 必须存在且匹配 `resourceType`,不尝试推断其他类型。
+- 目标当前的 `schedulerUuid` 必须与请求一致。
+- 解绑不校验计划时间。
+
+#### Workflow
+
+```text
+APIDetachPolicyRuleSchedulerMsg
+ |
+ |- [resourceType = SecurityGroup]:
+ | ChangeSecurityGroupSchedulerMsg(
+ | securityGroupUuid, schedulerUuid, operation=DETACH)
+ |
+ └─ [resourceType = VpcFirewallRuleSet]:
+ ChangeVpcFirewallRuleSetSchedulerMsg(
+ ruleSetUuid, schedulerUuid, operation=DETACH)
+```
+
+安全组和 VPC 防火墙规则集的查询结果返回 `schedulerUuid`,用于显示当前绑定关系。
+
+## 7. 本地消息设计与场景处理
+
+### 7.1 消息定义
+
+```text
+API Attach ──────────────────────> Change*SchedulerMsg(ATTACH)
+API Detach ──────────────────────> Change*SchedulerMsg(DETACH)
+API Update / Delete / 周期检查 ─> Change*SchedulerMsg(REFRESH)
+```
+
+```java
+interface PolicyRuleSchedulerMessage {
+ String getSchedulerUuid();
+}
+
+class DeletePolicyRuleSchedulerMsg extends NeedReplyMessage
+ implements PolicyRuleSchedulerMessage {
+ String schedulerUuid;
+}
+
+class ChangeSecurityGroupSchedulerMsg extends NeedReplyMessage
+ implements SecurityGroupMessage {
+ enum Operation { ATTACH, DETACH, REFRESH }
+
+ String securityGroupUuid;
+ String schedulerUuid;
+ Operation operation;
+}
+
+class ChangeVpcFirewallRuleSetSchedulerMsg extends NeedReplyMessage
+ implements VpcFirewallRuleSetMessage {
+ enum Operation { ATTACH, DETACH, REFRESH }
+
+ String ruleSetUuid;
+ String schedulerUuid;
+ Operation operation;
+}
+```
+
+`ATTACH`、`DETACH` 必须携带 `schedulerUuid`;`REFRESH` 不携带该字段,也不修改绑定关系。消息处理时读取最新规则和绑定,不携带计划快照。
+
+### 7.2 安全组消息
+
+```text
+ChangeSecurityGroupSchedulerMsg
+ |
+ └─ ChainTask["SecurityGroup-${securityGroupUuid}"]
+ |
+ └─ Flow["change-security-group-scheduler"]
+ |
+ |- [then, ATTACH / DETACH]: Flow["change-scheduler-binding"]
+ | |
+ | |- ATTACH: require empty binding, then set schedulerUuid
+ | |- DETACH: require matched binding, then clear schedulerUuid
+ | |
+ | └─ [rollback]: "restore-scheduler-binding"
+ | |
+ | |- restore schedulerUuid
+ | └─ apply rules with restored binding
+ |
+ └─ [then]: NoRollbackFlow["apply-security-group-rules"]
+ |
+ └─ SecurityGroupManagerImpl.RuleCalculator
+ |
+ |- query rules and VM NIC references
+ |- filter original Disabled rules
+ |- SecurityGroupRuleFilterExtensionPoint
+ | |
+ | └─ PolicyRuleSchedulerSecurityGroupExtension
+ | └─ PolicyRuleSchedulerFacade.isInSchedule(...)
+ |
+ └─ group by Host and apply complete rules
+```
+
+`SecurityGroupRuleFilterExtensionPoint` 同时接入 KVM 和 SDN 两条 `RuleCalculator` 规则生成路径。计划外返回空规则列表,但保留安全组 UUID 和 VM 网卡关系,用于清理 Host 上的旧规则。
+
+Host 下发失败沿用安全组现有处理,写入 `SecurityGroupFailureHostVO`,不触发绑定关系回滚;规则计算或下发前的控制面失败进入 Flow rollback。
+
+### 7.3 VPC 防火墙规则集消息
+
+```text
+ChangeVpcFirewallRuleSetSchedulerMsg
+ |
+ └─ ChainTask["Firewall-RuleSet-${ruleSetUuid}"]
+ |
+ └─ Flow["change-vpc-firewall-rule-set-scheduler"]
+ |
+ |- [then, ATTACH / DETACH]: Flow["change-scheduler-binding"]
+ | |
+ | |- ATTACH: require empty binding, then set schedulerUuid
+ | |- DETACH: require matched binding, then clear schedulerUuid
+ | |
+ | └─ [rollback]: "restore-scheduler-binding"
+ | |
+ | |- restore schedulerUuid
+ | └─ apply rules with restored binding
+ |
+ └─ [then]: NoRollbackFlow["apply-vpc-firewall-rules"]
+ |
+ |- query isApplied rules and firewall references
+ |- VpcFirewallRuleBuilder
+ | |
+ | └─ VpcFirewallRuleStateExtensionPoint
+ | |
+ | └─ PolicyRuleSchedulerVpcFirewallExtension
+ | └─ PolicyRuleSchedulerFacade.isInSchedule(...)
+ |
+ ├─ deleteRules = current applied rules
+ ├─ newRules = rules built for current schedule
+ └─ 按关联防火墙及 HA 对端发送 ApplyRuleSetChangesCmd
+```
+
+`VpcFirewallRuleStateExtensionPoint` 返回需要强制禁用的规则 UUID;`VpcFirewallRuleBuilder` 再与规则原有状态合并,不修改规则 VO。
+
+任一 VRouter 下发失败时,`ATTACH`、`DETACH` 恢复原绑定并按原绑定重新下发;`REFRESH` 返回错误,由调用方按最新数据库状态重试。
+
+### 7.4 场景处理
+
+- **计划在 Attach 处理期间结束**:计划队列执行时重新校验;计划已不存在未来生效分钟时不发送资源变更消息。
+- **绑定关系变更处理时资源已删除**:`ATTACH`、`DETACH` 返回资源不存在错误;晚到的 `REFRESH` 直接成功。
+- **计划引用不存在**:作为异常数据按未绑定计划生成规则并记录告警。
+- **同一计划同时影响多个资源**:按资源 UUID 去重并有界并发发送消息。
+
+| 操作 | 失败处理 |
+|---|---|
+| Attach/Detach | 恢复原绑定,再按原绑定刷新整个资源。 |
+| Update | 恢复旧计划,再按旧计划刷新全部关联资源。 |
+| Delete | 清空引用和删除计划后不回滚;记录失败,等待资源完整刷新。 |
+| 周期检查 | 不修改控制面,失败目标按最新状态重试。 |
+
+恢复动作失败时保留原始错误并记录告警,后续 VRouter 重连或完整配置刷新按最新数据库状态覆盖。
+
+## 8. 后端 UT自测
+
+只增加 Groovy 集成 Case,并通过注入时间主动触发周期检查,不等待真实分钟。
+
+```groovy
+class PolicyRuleSchedulerApiCase extends PremiumSubCase {
+ EnvPremiumSpec env
+
+ void testPolicyRuleSchedulerLifecycle() {
+ // 场景:用户完成定时计划的创建、全量修改、绑定、解绑和删除。
+ // 准备:创建安全组和防火墙规则集,并构造 UTC Once、本地时区 Weekly 普通时段和全天计划。
+ // 执行:创建并查询计划;将 Once 全量修改为 Weekly;分别绑定两个资源,
+ // 推进到计划结束后解绑安全组,再删除仍被规则集引用的计划。
+ // 校验:分钟、时区、日期、weekDays 和 00:00—00:00 原样返回;
+ // PUT 未提交的 description 被清空;已结束计划仍可解绑;
+ // 删除后计划不存在,规则集 schedulerUuid 为空。
+ }
+
+ void testPolicyRuleSchedulerAccountIsolation() {
+ // 场景:普通账号只能操作自己创建的定时计划。
+ // 准备:账号 A 创建计划并绑定自己的安全组,账号 B 创建另一个安全组。
+ // 执行:账号 B 查询、修改、删除账号 A 的计划,并尝试将该计划绑定到自己的安全组。
+ // 校验:账号 B 查询不到该计划,其余请求均失败;计划、原绑定及数据面状态不变。
+ }
+
+ void testInvalidSchedulerOperationsKeepExistingState() {
+ // 场景:用户提交非法时间表达式或对已有绑定执行冲突操作。
+ // 准备:创建一个有效计划并绑定安全组,记录计划、绑定和 Host 下发结果。
+ // 执行:分别提交非法时区、秒级时间、非法区间和非法 weekDays;再使用不匹配的
+ // resourceType、其他计划和错误 schedulerUuid 操作该安全组。
+ // 校验:请求均失败;计划内容、原 schedulerUuid 和 Host 规则保持不变;
+ // 已结束计划不能新绑定,失败操作不产生引用。
+ }
+
+ void testDeleteAccountCascadesPolicyRuleScheduler() {
+ // 场景:账号删除时级联删除其定时计划和已绑定的安全组、规则集。
+ // 准备:普通账号创建计划、安全组和防火墙规则集并完成绑定。
+ // 执行:删除账号。
+ // 校验:计划随账号删除,两类资源删除后数据库中无 schedulerUuid 残留。
+ }
+
+ void testSharedSchedulerLifecycleAcrossResources() {
+ // 场景:同一计划被多个安全组和防火墙规则集复用。
+ // 准备:两个安全组和两个防火墙规则集绑定同一计划,各资源均有原状态允许生效的规则。
+ // 执行:全量修改计划使当前分钟由计划外变为计划内,再保持绑定直接删除计划。
+ // 校验:Update 刷新全部四个资源并使规则生效;Delete 后计划不存在,
+ // 四个资源的 schedulerUuid 均为空且按未绑定计划刷新,资源及规则自身未被删除。
+ }
+
+ void testSharedSchedulerUpdateRollsBackAllResourcesOnFailure() {
+ // 场景:修改一份被多个资源复用的计划时,其中一个 VRouter 刷新失败。
+ // 准备:安全组和两个防火墙规则集绑定同一计划,旧计划处于计划外,新计划处于计划内;
+ // 记录各资源的下发结果,并让最后一个 VRouter 失败。
+ // 执行:全量修改计划。
+ // 校验:Update 返回原始刷新错误并恢复旧计划;此前已成功刷新的安全组、规则集和 VRouter
+ // 均再次按旧计划刷新,全部资源保持原 schedulerUuid,最终数据面与旧计划一致。
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testPolicyRuleSchedulerLifecycle()
+ testPolicyRuleSchedulerAccountIsolation()
+ testInvalidSchedulerOperationsKeepExistingState()
+ testDeleteAccountCascadesPolicyRuleScheduler()
+ testSharedSchedulerLifecycleAcrossResources()
+ testSharedSchedulerUpdateRollsBackAllResourcesOnFailure()
+ }
+ }
+}
+
+class SecurityGroupScheduledActivationCase extends PremiumSubCase {
+ EnvPremiumSpec env
+
+ void testSecurityGroupWorkdayScheduleLifecycle() {
+ // 场景:安全组在工作日期和工作时段内生效,计划外恢复原有启停语义。
+ // 准备:安全组包含 Enabled、Disabled 规则并绑定 Asia/Shanghai 时区、固定日期范围内
+ // 周一至周五 09:00—18:00 的 Weekly 计划。
+ // 执行:以 UTC 时刻覆盖本地日期范围外、周末、08:59、09:00、17:59、18:00;
+ // 再将计划改为 00:00—00:00 全天并解绑。
+ // 校验:只有日期、星期和时段同时匹配时下发 Enabled 规则;全天计划覆盖所选日全部分钟;
+ // Disabled 规则始终不下发,解绑后恢复原行为,规则 state 从未改变;
+ // 计划外空结果仍携带安全组 UUID并清理 Host 旧规则。
+ }
+
+ void testSecurityGroupLifecycleRefreshUsesCurrentSchedule() {
+ // 场景:安全组因 VM 网卡加入、VM 迁移和 Host 重连重新下发规则。
+ // 准备:两个 Host 上运行可迁移 VM,安全组绑定当前处于计划外的 Weekly 计划。
+ // 执行:依次将安全组加入 VM 网卡、迁移 VM、重连目标 Host;随后推进到计划内并再次重连。
+ // 校验:前三次完整规则均不包含定时规则;计划内重连包含原状态为 Enabled 的规则;
+ // 所有下发均读取执行时的计划,安全组和规则 state 不变。
+ }
+
+ void testSecurityGroupScheduleFiltersKvmAndSdnRules() {
+ // 场景:同一定时计划通过 KVM 和 SDN 两条安全组规则生成路径下发。
+ // 准备:分别创建使用 KVM、SDN 规则生成路径的 VM 网卡,并关联同一安全组;
+ // 安全组绑定当前处于计划外的计划。
+ // 执行:分别触发两类规则刷新,再推进到计划内重复刷新。
+ // 校验:计划外两条路径均过滤定时规则,计划内均恢复原 Enabled 规则;
+ // 两条路径使用同一计划结果且不修改规则 state。
+ }
+
+ void testSecurityGroupLargeRuleSetRefreshesPerHost() {
+ // 场景:包含大量规则的安全组在分钟边界按资源和 Host 批量刷新。
+ // 准备:两个 Host 上的 VM 使用同一安全组,安全组有 100 条 Enabled 规则;
+ // 安全组绑定 09:00—09:01 生效的 Once 计划并加入两个 VM 网卡;
+ // 在 08:59 触发一次周期检查后清空消息和 Host 模拟器计数。
+ // 执行:在 09:00 触发周期检查,在同一分钟重复检查,再推进到 09:01 检查。
+ // 校验:09:00 的 100 条规则状态切换只产生一个
+ // ChangeSecurityGroupSchedulerMsg(operation=REFRESH),
+ // 两个 Host 各收到一个 ApplySecurityGroupRuleCmd,且按端口包含全部 100 条
+ // 业务规则;同一分钟重复检查不产生刷新消息和 Host 请求;
+ // 09:01 仍只有一个 REFRESH 消息,每个相关 Host 只收到一个 ApplySecurityGroupRuleCmd,
+ // 100 条定时业务规则均不在下发结果中;
+ // 安全组的 schedulerUuid 和 100 条规则的原始 state 均未改变。
+ }
+
+ void testSecurityGroupSchedulerChangeWithHostFailure() {
+ // 场景:安全组绑定计划时一个 Host 下发失败,沿用安全组现有失败处理。
+ // 准备:两个 Host 上的 VM 使用同一安全组,其中一个 Host 首次下发失败。
+ // 执行:绑定计划,恢复失败 Host 后触发失败记录重试。
+ // 校验:绑定 API 成功并保存 schedulerUuid;成功 Host 使用计划结果;
+ // 失败 Host 生成 SecurityGroupFailureHostVO,重试读取当前绑定并最终一致。
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testSecurityGroupWorkdayScheduleLifecycle()
+ testSecurityGroupLifecycleRefreshUsesCurrentSchedule()
+ testSecurityGroupScheduleFiltersKvmAndSdnRules()
+ testSecurityGroupLargeRuleSetRefreshesPerHost()
+ testSecurityGroupSchedulerChangeWithHostFailure()
+ }
+ }
+}
+
+class VpcFirewallScheduledActivationCase extends PremiumSubCase {
+ EnvPremiumSpec env
+
+ void testVpcFirewallRuleSetWorkdayScheduleLifecycle() {
+ // 场景:一个规则集挂载到多个防火墙后,按工作日计划统一控制所有可用规则。
+ // 准备:两个 VPC 防火墙关联同一规则集,规则集包含 Enabled、Disabled 规则,
+ // 并绑定 UTC 时区、带固定日期范围的 Weekly 计划。
+ // 执行:覆盖日期、星期和分钟边界;在计划外和计划内分别触发 VRouter 完整配置刷新,
+ // 重复一次相同的完整刷新,并在同一分钟重复周期检查。
+ // 校验:计划外规则集内所有规则为 disable;计划内只有原有状态允许生效的规则为 enable;
+ // 两个关联 VRouter 均收到批量命令,未关联防火墙不收到请求;
+ // 重复批量替换结果一致,数据库 state、isApplied、expired 和规则集发布状态不变。
+ }
+
+ void testVpcFirewallScheduledRefreshUsesPublishedRuleVersion() {
+ // 场景:规则集存在尚未正式 Apply 的新版本时,定时刷新仍使用当前已发布版本。
+ // 准备:修改已发布规则,使旧规则为 isApplied=true、expired=true,新规则尚未应用;
+ // 规则集绑定当前处于计划内的定时计划。
+ // 执行:先触发定时刷新,再调用 APIApplyRuleSetChanges,随后再次触发完整刷新。
+ // 校验:正式 Apply 前的批量命令仍替换旧版本且不包含新规则;
+ // Apply 后的完整刷新使用新版本,定时刷新不改变版本字段。
+ }
+
+ void testVpcFirewallLargeRuleSetRefreshesInOneBatch() {
+ // 场景:包含 100 条规则的规则集跨过分钟边界。
+ // 准备:单个非 HA 防火墙关联包含 100 条 Enabled 规则的规则集,
+ // 规则集绑定 09:00—09:01 生效的 Once 计划。
+ // 执行:在 09:00、同一分钟和 09:01 分别触发周期检查。
+ // 校验:09:00 和 09:01 各产生一个
+ // ChangeVpcFirewallRuleSetSchedulerMsg(operation=REFRESH);
+ // 每次仅向该防火墙发送一条包含 100 条规则的 ApplyRuleSetChangesCmd;
+ // 同一分钟不重复发送,规则状态和发布状态不变。
+ }
+
+ void testVpcFirewallAttachAndDetachRollbackOnPartialFailure() {
+ // 场景:同一规则集的计划绑定和解绑分别遇到 VRouter 部分失败。
+ // 准备:规则集挂载到两个防火墙并记录未绑定结果。
+ // 执行:依次注入 Attach 失败、成功 Attach 和 Detach 失败;
+ // 每次均让第一个 VRouter 成功、第二个 VRouter 失败。
+ // 校验:失败 Attach 后 schedulerUuid 仍为空;失败 Detach 后保留原绑定;
+ // 第一个 VRouter 每次均恢复到操作前结果;
+ // 恢复命令失败时保留原始 API 错误,完整配置刷新后与当前数据库状态一致。
+ }
+
+ void testVpcFirewallPeriodicRefreshRetriesLatestState() {
+ // 场景:分钟边界刷新两个 VRouter 时部分失败,重试前计划再次跨过边界。
+ // 准备:规则集绑定 09:00—09:01 生效的 Once 计划并关联两个 VRouter;
+ // 第一个 VRouter 正常,第二个 VRouter 在 09:00 刷新失败。
+ // 执行:09:00 触发周期检查;恢复第二个 VRouter 后在 09:01 再次触发。
+ // 校验:09:00 已成功的 VRouter 不因部分失败回退;09:01 两个 VRouter 均按当前计划
+ // 收到 disable,失败目标不补发已经过时的 enable,待重试记录被清除。
+ }
+
+ void testDeleteSchedulerKeepsCascadeResultOnRefreshFailure() {
+ // 场景:删除仍被规则集引用的计划时,VRouter 刷新失败。
+ // 准备:规则集绑定当前生效的计划,删除时注入 VRouter 请求失败。
+ // 执行:删除计划,随后恢复并重连 VRouter 以触发完整配置刷新。
+ // 校验:删除 API 成功,计划不存在,规则集 schedulerUuid 为空;
+ // 删除结果不因数据面失败回滚,
+ // 重连后规则集按未绑定计划状态生效。
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testVpcFirewallRuleSetWorkdayScheduleLifecycle()
+ testVpcFirewallScheduledRefreshUsesPublishedRuleVersion()
+ testVpcFirewallLargeRuleSetRefreshesInOneBatch()
+ testVpcFirewallAttachAndDetachRollbackOnPartialFailure()
+ testVpcFirewallPeriodicRefreshRetriesLatestState()
+ testDeleteSchedulerKeepsCascadeResultOnRefreshFailure()
+ }
+ }
+}
+
+class PolicyRuleSchedulerMultiManagementNodeCase extends PremiumSubCase {
+ EnvPremiumSpec env
+
+ void testPeriodicTaskOwnershipAndFailover() {
+ // 场景:双 MN 中只有 Owner 扫描,Owner 切换后重新计算当前计划结果。
+ // 准备:创建已绑定和未绑定计划的安全组、规则集,由 MN-1 完成一次扫描,
+ // 并保留一个周期刷新失败的资源。
+ // 执行:两个 MN 在同一分钟触发任务;随后停止 MN-1 并让 MN-2 接管。
+ // 校验:接管前只有 MN-1 发送消息;MN-2 首轮刷新全部安全组和规则集,
+ // 失败资源按当前数据库状态恢复;后续只刷新状态变化或等待重试的资源。
+ }
+
+ void testPeriodicTaskContinuesAfterScanFailure() {
+ // 场景:某轮扫描抛出异常后,周期任务不会停止或跳过时间边界。
+ // 准备:记录上次成功检查分钟 T1,并让本轮安全组或防火墙规则集计算抛出异常。
+ // 执行:连续触发失败分钟和下一分钟的周期任务。
+ // 校验:失败轮次不更新 T1,周期任务没有被取消;下一轮仍从原 T1 计算状态变化并发送刷新。
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testPeriodicTaskOwnershipAndFailover()
+ testPeriodicTaskContinuesAfterScanFailure()
+ }
+ }
+}
+
+class PolicyRuleSchedulerConcurrencyCase extends PremiumSubCase {
+ EnvPremiumSpec env
+
+ void testConcurrentAttachUsesResourceQueue() {
+ // 场景:两个不同计划同时绑定同一个资源。
+ // 准备:创建两个可绑定计划和一个未绑定目标资源。
+ // 执行:并发向同一安全组绑定两个计划,再对防火墙规则集重复该场景。
+ // 校验:每个资源只有一个 Attach 成功,另一个返回已绑定;schedulerUuid、Host/VRouter
+ // 最终状态均对应成功计划,没有混合两次请求的结果。
+ }
+
+ void testSchedulerOperationsUseSchedulerQueue() {
+ // 场景:同一计划的 Update、Attach 和 Delete 来自不同 MN。
+ // 准备:阻塞 Update,并在另一个 MN 依次提交 Attach 和 Delete。
+ // 执行:确认后续请求未越过 Update,再释放阻塞。
+ // 校验:三个操作按进入计划队列的顺序执行;Delete 最终清空全部匹配引用并删除计划;
+ // 排在 Delete 之后的 Attach 返回计划不存在,不产生孤儿引用。
+ }
+
+ void testSecurityGroupRuleChangeDuringScheduledRefresh() {
+ // 场景:安全组计划刷新与规则修改并发。
+ // 准备:阻塞 Attach 的 Host 刷新响应,随后提交规则 state 修改和 Detach。
+ // 执行:释放 Host 响应并等待资源队列处理完成。
+ // 校验:最终 schedulerUuid 为空、规则 state 等于修改值;最后一次 Host 规则命令与该末态一致;
+ // 规则自身不增加 schedulerUuid。
+ }
+
+ void testVpcFirewallRuleChangeDuringScheduledRefresh() {
+ // 场景:规则集计划刷新与规则增删改、启停并发。
+ // 准备:周期刷新已开始并阻塞 VRouter 响应。
+ // 执行:提交创建规则、修改规则、启停规则和修改规则集操作后释放刷新。
+ // 校验:上述操作按提交顺序完成;最后一次批量规则命令与数据库末态一致,
+ // 没有旧刷新覆盖后续用户操作。
+ }
+
+ void testAttachAndUpdateUseSameSchedulerQueue() {
+ // 场景:Attach 与把计划修改为已结束状态的 Update 并发。
+ // 准备:分别构造 Update 先进入队列和 Attach 先进入队列两个资源。
+ // 执行:并发提交两个 API。
+ // 校验:Update 先执行时 Attach 因计划不可绑定失败;Attach 先执行时绑定成功,
+ // 随后的 Update 保留绑定并按已结束计划刷新,不出现处理中途二次判定。
+ }
+
+ void testLateRefreshUsesCurrentBinding() {
+ // 场景:旧计划产生的刷新消息晚于解绑和重新绑定到达资源队列。
+ // 准备:安全组和防火墙规则集均绑定计划 A,阻塞两类 REFRESH 消息进入资源队列;
+ // 计划 A 当前生效,计划 B 当前不生效。
+ // 执行:两个资源依次解绑计划 A、绑定计划 B,再释放旧 REFRESH 消息。
+ // 校验:两个资源仍绑定计划 B;晚到消息按计划 B 生成最终规则,
+ // 不恢复计划 A 的引用或数据面结果。
+ }
+
+ void testLateSecurityGroupRefreshAfterResourceDeletion() {
+ // 场景:安全组周期刷新进入资源队列前,安全组已被用户删除。
+ // 准备:通过周期检查产生 ChangeSecurityGroupSchedulerMsg(operation=REFRESH),
+ // 并阻塞其进入安全组资源队列。
+ // 执行:调用公开 API 删除安全组,再释放刷新消息。
+ // 校验:刷新消息成功返回且不向 Host 下发;不新增 SecurityGroupFailureHostVO,
+ // 已删除安全组和关联记录均未被重新创建。
+ }
+
+ void testLateVpcFirewallRuleSetRefreshAfterResourceDeletion() {
+ // 场景:防火墙规则集周期刷新进入资源队列前,规则集已被用户删除。
+ // 准备:通过周期检查产生 ChangeVpcFirewallRuleSetSchedulerMsg(operation=REFRESH),
+ // 并阻塞其进入规则集资源队列。
+ // 执行:调用公开 API 删除规则集,再释放刷新消息。
+ // 校验:刷新消息成功返回且不向 VRouter 下发;资源不进入周期任务待重试集合,
+ // 已删除规则集和关联记录均未被重新创建。
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testConcurrentAttachUsesResourceQueue()
+ testSchedulerOperationsUseSchedulerQueue()
+ testSecurityGroupRuleChangeDuringScheduledRefresh()
+ testVpcFirewallRuleChangeDuringScheduledRefresh()
+ testAttachAndUpdateUseSameSchedulerQueue()
+ testLateRefreshUsesCurrentBinding()
+ testLateSecurityGroupRefreshAfterResourceDeletion()
+ testLateVpcFirewallRuleSetRefreshAfterResourceDeletion()
+ }
+ }
+}
+```
+
+## 9. 开发量评估
+
+| 工作项 | 预计人日 |
+|---|---:|
+| `PolicyRuleScheduler` 表、全量 API、校验和 Cascade 删除 | 2—3 |
+| 共享时间判定、周期检查任务、本地消息和双 MN 处理 | 2 |
+| 安全组 MN 处理及 `zstack-utility` 小范围调整 | 2—3 |
+| 防火墙规则集规则构建、批量刷新及 `zstack-vyos` 幂等适配 | 2—3 |
+| 后端 Groovy UT、联调和回归 | 3—4 |
+| **合计** | **11—15 人日** |
+
+单后端约 2.5—3 周,不含前端开发。
+
+## 10. 参考资料
+
+- [SUG-2668](http://jira.zstack.io/browse/SUG-2668) — 产品需求。
+- [安全组优化-后端](http://confluence.zstack.io/pages/viewpage.action?pageId=144797977) — 功能点、API 和数据库设计格式参考。
+- `zstack/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java` — API 转本地消息、资源队列持有、Flow 失败恢复和删除 Cascade 语义参考。
+- `zstack/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/APIAttachSecurityGroupToL3NetworkMsg.java`、`APIDetachSecurityGroupFromL3NetworkMsg.java` — Attach/Detach 关系 API 的命名和校验语义。
+- `zstack/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java`、`SecurityGroupManagerImpl.java`、`RefreshSecurityGroupRulesOnHostMsg.java`、`RefreshSecurityGroupRulesOnVmMsg.java` — `SecurityGroupMessage` 路由、资源队列和现有规则刷新入口。
+- `zstack-utility/kvmagent/kvmagent/plugins/securitygroup_plugin.py` — 安全组规则和 iptables 链处理。
+- `zstack/core/src/main/java/org/zstack/core/thread/DispatchQueueImpl.java`、`zstack/core/src/main/java/org/zstack/core/cloudbus/ResourceDestinationMaker.java` — 本地同步队列和双 MN 资源路由。
+- `zstack/core/src/main/java/org/zstack/core/db/SQLBatch.java`、`zstack/core/src/main/java/org/zstack/core/cascade/CascadeFacadeImpl.java`、`zstack/identity/src/main/java/org/zstack/identity/AccountBase.java` — 控制面批量修改和 Cascade 的失败边界。
+- `zstack/header/src/main/java/org/zstack/header/vo/ResourceVO.java`、`ResourceTypeMetadata.java`、`zstack/header/src/main/java/org/zstack/header/aspect/OwnedByAccountAspect.aj` — 账号资源的继承映射、资源类型和账户资源引用。
+- `zstack/premium/mevoco/src/main/java/org/zstack/header/scheduler/SchedulerTriggerVO.java`、`zstack/premium/mevoco/src/main/java/org/zstack/scheduler/SchedulerFacadeImpl.java`、`SchedulerCascadeExtension.java` — 同类计划资源的创建和账号级联删除参考。
+- `zstack/premium/mevoco/src/main/java/org/zstack/scheduler/SchedulerApiInterceptor.java`、`AbstractSchedulerJob.java` — Scheduler 资源路由和通用 Scheduler 任务历史实现参考。
+- `zstack/premium/plugin-premium/container/src/main/java/org/zstack/container/ContainerManagerImpl.java`、`zstack/core/src/main/java/org/zstack/core/rest/RESTApiFacadeImpl.java` — 固定任务标识配合 `ResourceDestinationMaker` 的现有实现参考。
+- `zstack/premium/plugin-premium/vpcFirewall/src/main/java/org/zstack/vpcfirewall/VpcFirewallApiInterceptor.java`、`VpcFirewallManagerImpl.java`、`VpcFirewallRuleSetBase.java`、`VpcFirewallBackend.java`、`entity/VpcFirewallCommands.java`、`entity/VpcFirewallRuleSetL3RefVO.java` — 防火墙规则集消息路由、资源队列、发布流程和批量下发入口。
+- `zstack/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleStateCase.groovy`、`zstack/premium/test-premium/src/test/groovy/org/zstack/test/integration/premium/vpc/firewall/VpcFirewallLifeCycleCase.groovy` — Groovy Case 的场景方法和 `test()` 编排风格。
+- `zstack-vyos/plugin/firewall.go` — VPC 防火墙规则状态处理。