# CMDB 运维手册

> 适用范围：CMDB Phase 1 — 云服务器 / 域名 / DNS / LB / CDN / 对象存储 / IAM 用户 / 月度账单管理  
> 最近更新：**2026-05-17（migration 173）**：网络资产历史数据迁移 + 修改人字段 + LB owner FK + DNS enable/disable 审批 + Route 53 Domains AccessDenied 优雅降级；详见末尾 [migration 173 操作步骤](#migration-173-legacy-cmdb-数据迁移-2026-05-17) 一节  
> 2026-04-24（migration 066）：腾讯云 + UCloud 两个连接器升为「生产级」——  腾讯云补齐 CVM + 域名注册 + CAM IAM 全量 + 月度账单 + CAM 离职删号；UCloud 从 stub 升到全功能驱动（UHost/UDNR/ULB/UCDN/UFile + IAM User + ListUBillDetail 日级账单 + 离职删号）；COS SDK `cos-python-sdk-v5` 写进 pyproject  
> 2026-04-24：云资产总览「立即同步」现在一键包含反向权限 + auto-owner；auto-owner 兜底到"按 granted_at 升序取第一个"，无 owner 等价角色时降级到任意 active grant 持有人；`/admin/scheduled-jobs` 搜索不再匹配 description，新增 `/admin/scheduled-jobs/runs` 全局执行日志页  
> 2026-04-23：migration 058：`legacy_cmdb` 按 `vendor_id` 拆成 UCloud + 金山云 独立租户；新增 `ucloud` / `ksyun` / `alicloud_intl` / `gcp-primary` / `azure-primary` 五个 driver seed；`/admin/scheduled-jobs` 顶部加 Segmented 分类筛选；清理泄漏的 `test-aliyun` 数据。

---

## 云成本归属夜间重算：TiDB 8176 内存取消（2026-08-10）

**修复**：迁移 458 新增 `ix_facts_date_id(date, id)`；重算不再以全局 ID
跨越 90 天窗口的历史空洞，而是按会计日、当天 ID 游标翻页。每条读取都是
`date = :day AND id > :cursor ORDER BY id LIMIT 10000` 的窄索引范围，避免单条
查询扫描大量窗口外历史行。任务保持幂等、每批提交；部署后下一个夜间 tick 会
自动补齐，无需手工修复归属数据。

**验证**：部署后查看该任务的下一条 `scheduled_job_runs`：应为成功，且不再有
8176。不要并发手工触发重算；cron、HTTP、MCP 已共用单飞锁。

---

## 零、云 + 网络资产总览 Dashboard（2026-04-22 多云扩展）

**入口**：
- 管理员：云资产 → **云资产总览** (`/admin/cloud/overview`) + 网络资产 → **网络资产总览** (`/admin/network/overview`)
- 员工：我的云资产 → **云资产总览** (`/my/cloud/overview`) + 我的网络资产 → **网络资产总览** (`/my/network/overview`)

**后端**：
- `GET /api/v1/cmdb/cloud-tenants` — 轻量租户列表（id/name/slug/provider/enabled），`CloudTenantFilter` 组件调用；员工可读
- `GET /api/v1/cmdb/analytics` — 管理员 / 审计聚合数据
- `GET /api/v1/cmdb/analytics/my` — 当前用户归属数据（`owner_person_id` 自动过滤）
- 两个 analytics 接口都接受 `integration_id` 参数做单租户钻取

**多云支持**（production-grade）：
- 所有云/网络页面顶部有 **租户筛选 pill 条** — AWS / AWS-SG / 阿里云 / AliCloud 海外 / 腾讯云 / UCloud / 金山云(历史) / GCP / Azure 逐个切换；禁用的租户可点也可查
- **All tenants** 模式：图表用 `by_integration` 展开（两个 AWS 租户分两条，不合并成一个 `aws`）
- **单租户钻取**：图表切成按区域/状态/网络类型/源站类型等细粒度
- **租户卡矩阵**（管理员 + All）：每个云租户一张卡（名字、provider、slug、启用、7 种资源数、最近月份账单、最近同步时间），点卡即钻取

**云资产总览内容**：Totals + 服务器（按租户/状态/Top8 区域/OS）+ 域名到期桶 + LB/CDN/OSS + 账单趋势（按租户堆叠）+ IAM

**网络资产总览内容**：Totals（含 ICP 已备案率）+ **6 个月续费 Runway**（当月红/下月橙）+ 域名（按租户/到期桶/**ICP 备案覆盖率**/注册商 TOP5）+ DNS（按租户/记录类型/Top 10 zones）+ CDN（按租户/**HTTPS 覆盖率**/回源类型）

**告警**：过期域名 → 红；≤7d 到期 → 橙

**owner-scope 诚实原则**：`cmdb_load_balancers` / `cmdb_cdn_domains` / `cmdb_object_storage` 无 `owner_person_id` 列，员工视图对这三类强制返回 0（不用 fleet 数）。

**子页面多云筛选**：云资产（服务器 / LB / OSS / IAM / 账单）+ 网络资产（域名 / DNS / CDN）的列表页顶部都有 `<CloudTenantFilter>`，选 tenant 时 `setPage(1)` + `integration_id` 过滤查询 + 同步过滤导出。

**`/admin/scheduled-jobs` 分类筛选**（2026-04-23）：顶部 Segmented 控件按 handler_key 的 namespace 把所有任务拆成 5 个类别 —「入离职 / HR」「云 / CMDB」「权限开通/回收」「通知」「其他」，每个 pill 上显示计数。点 pill → 列表只显示该类别；后端用 `category=<bucket>` 参数做 server-side 过滤，pagination 保持准确。搜索框、启用状态、上次状态三个条件和分类 AND 组合。

**`/admin/scheduled-jobs/runs` 全局执行日志页**（2026-04-24）：卡片 header 上多了一个「查看全部执行日志」按钮跳过去。页面跨任务聚合 `scheduled_job_runs`，默认显示最近 24h 内的**失败**记录（改 Select 就能看成功/全部），可按分类 / 触发源（`scheduler` / `manual` / `startup`）/ 关键字（只搜 job name + display_name + handler_key）/ 时间窗（1h / 24h / 7d / 全部）组合过滤。每行展开看 `error_message` 全文 + `result_summary` JSON。想看单任务历史仍然在旧的时钟按钮抽屉里。

**`/admin/scheduled-jobs` 搜索行为变更**（2026-04-24）：keyword 现在只匹配 `name` / `display_name` / `handler_key`，**不再匹配 description**。之前搜 `azure` 会误召回 `cmdb.grants.aws-sg`（因为其 description 里列举了所有云厂商的反向权限源）；去掉 description 后搜索结果只命中 slug/handler_key 里真的含这个字样的任务。description 仍然在行展开视图里可见，只是不参与搜索。

**故障排查**：
- 所有卡片 0 → 云同步没跑；去 `/admin/scheduled-jobs` 查 `cmdb.sync.*`
- 账单趋势空 → `cmdb_billing_monthly` 空；检查 `cmdb.billing.*`
- 某租户卡显示 0 但云上有 → 该能力没开，去 `/admin/integrations` 把 capability 打开
- 员工视图资源少 → `owner_person_id` 没回填
- 租户 pill 没东西 → 没有 `kind=cloud` 接入

---

## 架构变更（2026-04-22，migration 047）

**云同步现在按「(云账号 × 资源类型) 各一条 scheduled_job」的粒度运行。** 旧的全局 `cmdb.cloud_sync` handler 被删除，替换为 **通用 handler `cmdb.resource_sync`**，由 `config.{integration_id, capability}` 决定做什么。每新建一个 `integrations(kind='cloud')` 行都会在 `/admin/scheduled-jobs` 页面自动冒出一批 `cmdb.sync.<slug>.<capability>` 行（按 `integration.capabilities` 列表生成）+ 一条 `cmdb.billing.<slug>` 账单行；改了 `capabilities` 就做 diff，删除 integration 会级联清理。

**同步历史菜单 `/admin/cloud/sync-jobs` 已下线**。每能力的执行记录通过 `/admin/scheduled-jobs` 下钻查看（`scheduled_job_runs` 表自带历史）；原子级的 `CmdbSyncJob` 行仍然存在，只是没有专属页面了，通过 `/cmdb/sync-jobs` REST API 仍可读。

**新增能力**：`ProviderCapability.IAM`（AWS 已实现 `list_iam_users`；Aliyun RAM / 腾讯 CAM 留空占位）。新表 `cmdb_iam_users` 与 `cmdb_billing_monthly`。

**Bedrock 自动授权链路**：当 API Key 申请的 provider.name 匹配 "AWS Bedrock" 且 Feishu 审批状态转为 approved，`bedrock_autogrant.py` 自动在 AWS SG 租户下创建 IAM 用户 + 挂 `bedrock:InvokeModel` 策略，AK/SK 回填到 `assignment.key_value` 并把 status 推到 `active`。

**所有 CMDB 资源表（`cmdb_servers`、`cmdb_domains`、`cmdb_dns_records`、`cmdb_load_balancers`、`cmdb_cdn_domains`、`cmdb_object_storage`、`cmdb_iam_users`、`cmdb_billing_monthly`、`cmdb_sync_jobs`）的外键列统一叫 `integration_id`，指向 `integrations.id`。**

---

## 一、环境依赖

| 工具 | 用途 | 安装 |
|------|------|------|
| `cloud-sql-proxy` | 连接旧 CMDB GCP Cloud SQL | `brew install cloud-sql-proxy` |
| gcloud 认证 | Cloud SQL Proxy IAM 授权 | `~/.config/gcloud/application_default_credentials.json` |
| `pymysql` | 迁移脚本依赖 | `cd server && uv sync` 安装到项目本地环境 |

### 旧 CMDB 数据库信息

```
实例:  gcp-digital-infra:asia-east2:pch-sql-db
用户:  cmdb
密码:  从 ops secret store 或本地 `.env` 的 `CMDB_DB_PASSWORD` 获取
库名:  cmdb
```

---

## 二、历史数据迁移

### 前置步骤：启动 Cloud SQL Proxy

```bash
GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json \
  cloud-sql-proxy --address 127.0.0.1 --port 3307 gcp-digital-infra:asia-east2:pch-sql-db &
```

确认 Proxy 就绪：输出 `The proxy has started successfully and is ready for new connections!`

### 运行迁移

```bash
cd <helix-worktree>/server

# 预检（不写入）
CMDB_DB_PASSWORD='<password>' \
DATABASE_URL_SYNC='mysql+pymysql://...' \
uv run python scripts/migrate_cmdb.py --phase preflight

# 全量迁移
CMDB_DB_PASSWORD='<password>' \
DATABASE_URL_SYNC='mysql+pymysql://...' \
uv run python scripts/migrate_cmdb.py --phase all
```

### 迁移阶段说明

| Phase | 说明 |
|-------|------|
| `preflight` | 检查源/目标表结构、owner 覆盖率，不写入 |
| `all` | preflight + user_mapping + domains + servers + validate |
| `domains` | 仅迁移域名 |
| `servers` | 仅迁移服务器 |
| `validate` | 数据对比校验（行数、FK、owner 解析率）|

### 迁移结果（2026-04-12 首次执行）

```
domains:  src=40  dst=40  ✓
servers:  src=657 dst=657 ✓
FK violations: 0
Owner resolution: 24/697 resolved（大部分 NULL，原因：HRIS 数据尚未完整同步）
```

> **Owner 回填**：待 HR 同步完善后，执行 `UPDATE cmdb_servers s JOIN identity_persons p ON p.work_email = ... SET s.owner_person_id = p.id` 回填。

### 历史数据挂载到哪个云（2026-04-23 起）

migration 058 起，`legacy_cmdb` 这条 catch-all integration **已被删除**，原先挂在它下面的所有行按 `raw_data.vendor_id` 拆成两个独立云租户：

| 来源 `vendor_id` | 目标 integration | 数据类型 |
|---|---|---|
| `1` | **金山云 (历史)** (`ksyun-legacy`) | 服务器 541 台 + 负载均衡 13 个 |
| `2` | **UCloud** (`ucloud-primary`) | 服务器 116 台 + 负载均衡 26 个 + CDN 5 条 |
| 其他 / NULL | **UCloud** (同上) | 域名 40、DNS 897、非 UCloud CDN 22（历史兜底） |

两条 integration 都 `enabled=false` 且没有 capability:

- **UCloud** (`ucloud-primary`)：**2026-04-24 起升级为生产级**,driver 实现了 UHost / UDNR / ULB / UCDN / UFile + IAM User + ListUBillDetail 日级账单 + 离职删号。`supports_test_connection=true`(走 `GetRegion` 探活),`capabilities = servers / dns_records / load_balancers / cdn / object_storage / user_revoke`。填完 `public_key` + `private_key` 后 `enabled=1` 即可。**UDNR 同步需要** 在连接器配置的 `dns_zones` 字段填入域名列表(UCloud 没有 list-all-zones API);留空则 DNS 同步 skip,历史数据原样保留。域名注册未实现(UCloud 没有 list-all-domains API),历史 40 条域名数据只读展示。
- **金山云** (`ksyun-legacy`)：只读历史归档，不支持任何 capability、不支持连接测试、不支持多实例。仅用于数据可视化，不接受 admin 添加凭证。

`migrate_cmdb.py` 已经更新为直接写到这两条 integration（不再落到 `legacy_cmdb` 再拆）。**前提：target DB 必须已经 `alembic upgrade head`**（即 migration 058 已执行，`ucloud-primary` / `ksyun-legacy` 两行存在），否则脚本会 fail fast。

> 迁移脚本也会把泄漏到 DB 的 pytest 测试 integration（`name='test-aliyun'` / slug `test-aliyun-*`）连带清理。pytest fixture 已改名为 `pytest-fixture-aliyun`。

---

## 三、云账号管理（生产环境）

### 3.0 一键 seed AWS SG（Singapore）(新)

`scripts/seed_aws_sg.py` 是 idempotent 的 —— 本地、生产都可以跑。**AK/SK 走环境变量，绝不进 git**：

```bash
# 本地（确保 docker compose 的 MySQL 已起）
cd <helix-worktree>/server
set -a; source ../.env; set +a
AWS_SG_ACCESS_KEY_ID=<rotated-ak> \
AWS_SG_SECRET_ACCESS_KEY=<rotated-sk> \
uv run python ../scripts/seed_aws_sg.py
# 第一次输出: [seed] creating cloud integration slug='aws-sg'
# 再跑一次: [seed] updating cloud integration slug='aws-sg'

# 生产 (SSH 到 prod EC2 后)
cd /srv/helix/server
AWS_SG_ACCESS_KEY_ID=<rotated-ak> \
AWS_SG_SECRET_ACCESS_KEY=<rotated-sk> \
DATABASE_URL='mysql+aiomysql://...tidb_cloud.../helix?ssl=true' \
CREDENTIAL_ENCRYPTION_KEY='<prod key>' \
uv run python ../scripts/seed_aws_sg.py
```

跑完自动发生：
- `integrations` 表里有一行 slug=`aws-sg`，capabilities 含 `iam`；
- `/admin/scheduled-jobs` 里出现 8 行（7 个 `cmdb.sync.aws-sg.<cap>` + 1 个 `cmdb.billing.aws-sg`）；
- `/admin/cloud/iam-users` 下一次同步后开始显示数据；
- `/admin/cloud/billing` 下一次月度 cron（每月 2 号 2:xx）后有数据（或者手动在 scheduled-jobs 页点"立即执行"）。

### 3.0.1 启用 standard AWS（management 账号 / `aws-primary`）（2026-04-29 新增）

**前置条件**：
- 本地或生产 DB 里已有 `slug='aws-primary'` 的 cloud integration 占位行（migration 045 起预置）
- 一对 management-account IAM 用户的 access key（必须有 `organizations:ListAccounts`、`organizations:CreateAccount`、`sso-admin:*`、`identitystore:*`、`ce:GetCostAndUsage`、`iam:List*` 权限）
- IAM Identity Center instance 的三元组：`InstanceArn` / `IdentityStoreId` / region（PingCAP 的 instance 在 `ap-northeast-1`）

**发现 Identity Center 三元组**：

```bash
for r in us-east-1 us-west-2 ap-southeast-1 ap-northeast-1 eu-west-1; do
  AWS_ACCESS_KEY_ID=<ak> AWS_SECRET_ACCESS_KEY=<sk> \
    aws sso-admin list-instances --region "$r" --output text 2>/dev/null \
    | awk -v r="$r" '/INSTANCES/ {print r, $2, $3}'
done
```

**配置（admin UI）**：登录后到 **配置中心 → 整合连接器**，编辑 slug=`aws-primary` 的 AWS 行，填入：

| 字段 | 值 |
|------|----|
| `access_key_id` | management 账号 IAM AK |
| `secret_access_key` | management 账号 IAM SK |
| `default_region` | `us-east-1`（Cost Explorer / Organizations / IAM 都是全局服务，挂哪个 region 都行） |
| `account_mode` | `sso`（默认） |
| `sso_instance_arn` | `arn:aws:sso:::instance/ssoins-…` |
| `sso_identity_store_id` | `d-…` |
| `sso_region` | `ap-northeast-1` |
| capabilities | 全勾：`servers / domains / dns_records / cdn / object_storage / load_balancers / iam / user_revoke` |

启用后保存，**整合连接器**会自动 reconcile 出 12 个 scheduled_jobs 行：

```
cmdb.sync.aws-primary.{servers,domains,dns_records,cdn,object_storage,load_balancers,iam}  # 每小时
cmdb.scopes.aws-primary       # 每日 04:xx — 拉 AWS Org 子账号
cmdb.roles.aws-primary        # 每日 04:xx — 拉 Identity Center permission set 目录
cmdb.grants.aws-primary       # 每日 05:xx — 反向拉 Identity Center assignment 并自动选 owner
cmdb.billing.aws-primary      # 每月 2 号 02:xx — Cost Explorer 月账单
cmdb.billing_daily.aws-primary  # 每日 06:xx — 日级账单 + 异常检测告警
```

**首跑**：在 `/admin/scheduled-jobs` 列表里依次找到 `cmdb.scopes.aws-primary` → `cmdb.sync.aws-primary.iam` → `cmdb.grants.aws-primary` → `cmdb.billing.aws-primary` 点 **立即执行**，按顺序跑。`cmdb.scopes` 跑完会发现所有 AWS Organizations 成员账号；`cmdb.grants` 跑完会自动选出 owner（优先 AdministratorAccess 持有者，平局取最早 `granted_at`）。

**员工申请新 AWS 子账号**：员工在 `/me/cloud-requests` 选 AWS 这个 connector → 提交。IT Admin 在 `/admin/cloud/scope-requests` 审批通过后，后台自动调 `Organizations.CreateAccount` 并轮询 `DescribeCreateAccountStatus` 最长 6 分钟，成功后 `cmdb_cloud_scopes` 写入新行，申请人挂为 owner，平台默认月预算和告警规则一并套上。失败的请求 stuck 在 `failed`，IT Admin 可以从 `/admin/cloud/scope-requests/{id}/manual-provision` 手动收尾。

**注意**：`aws-primary`（standard AWS, account 317766989874）和 `aws-sg`（AWS Singapore, single-tenant IAM）是**两个完全独立的 AWS 账号**，凭证不互通；不要复用上面的步骤去重新配 `aws-sg`（那条用 § 3.0 的 seed 脚本）。

### 3.1 添加真实云账号（首选：UI）

进入 `/admin/integrations`，点右上角"新建连接器" → 选 kind=云 → 选 provider → 填 name、凭证字段（字段由 driver 的 `config_schema` 决定）→ 可选 capabilities 多选（留空等于全部）→ 保存。

当前注册的 cloud driver + 它们的 capability 清单：

| provider | display_name | capabilities | 说明 |
|---|---|---|---|
| `aws` | AWS | servers / domains / dns / cdn / oss / lb / iam / user_revoke | 既可作为 Org 主账号 (sso)，也可作为单租户 (iam，比如 AWS SG) |
| `aliyun` | 阿里云 | servers / domains / dns / cdn / oss / lb | 国内阿里云主账号 |
| `alicloud_intl` | AliCloud (海外) | servers / domains / dns / cdn / oss / lb | 海外阿里云(`aliyun` 的子类，默认 region=ap-southeast/us-east/eu-central) |
| `tencent` | 腾讯云 | servers / domains / dns / lb / cdn / oss / iam / user_revoke | CAM 子账号模式;CVM / DNSPod / CLB / CDN / COS / 域名注册 / CAM ListUsers / 月度账单(DescribeBillDetail) |
| `gcp` | Google Cloud Platform | servers / oss | 需要 Service Account JSON |
| `azure` | Microsoft Azure | servers / oss | 需要 Tenant ID + Client ID + Client Secret |
| `ucloud` | UCloud | servers / dns / lb / cdn / oss / user_revoke | UHost / ULB / UCDN / UFile + IAM User;DNS 需要 `dns_zones` 配置(UCloud 无 list-all-zones API);域名注册不支持(UCloud 无 list-domains API) |
| `ksyun` | 金山云 (历史) | 暂无 | 只读历史归档 |

seed 行（migration 034 + 058）首次启动已经出现在列表里（`aws-primary`、`aws-sg`、`aliyun-primary`、`tencent-primary`、`alicloud-intl-primary`、`gcp-primary`、`azure-primary`、`ucloud-primary`、`ksyun-legacy`），管理员只需编辑对应的凭证再 `enabled=true` 即可。

保存后若想验证凭证是否可用，点行里的"测试连接"按钮。

### 3.2 添加真实云账号（API，用于脚本）

```bash
# 示例：阿里云
curl -X POST http://localhost:8000/api/v1/integrations \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "cloud",
    "provider": "aliyun",
    "name": "Aliyun Prod",
    "slug": "aliyun-prod",
    "description": "生产环境阿里云账号",
    "enabled": true,
    "secrets": {
      "access_key_id": "LTAI...",
      "access_key_secret": "..."
    },
    "capabilities": ["servers", "domains", "dns_records", "load_balancers", "cdn", "object_storage"]
  }'
```

### 3.3 手动触发一次同步

```bash
INTEGRATION_ID="<uuid>"
curl -X POST http://localhost:8000/api/v1/cmdb/sync/$INTEGRATION_ID/trigger \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"resource_types": ["*"]}'
# 返回 202，后台异步执行
```

UI 对应入口：`/admin/integrations` → 云 tab → 行里的"触发同步"。

### 3.4 定时同步（per-capability scheduled_jobs）

migration 047 起，每个 (云账号 × 资源类型) 是一条独立 scheduled_job 行，名字形如 `cmdb.sync.<slug>.<capability>`，都挂到 `cmdb.resource_sync` handler；cron minute 按名字 hash 分散到 0-59 避免爆发。另外每个云账号还有一条 `cmdb.billing.<slug>` 月度账单行，每月 2 号 2:xx 执行。

- **新建云账号 → 自动冒出这些行**（`IntegrationService.create()` 钩子）。
- **改 `capabilities` → diff add/remove**（`IntegrationService.update()` 钩子）。
- **删除云账号 → 级联清理** 对应行。
- **管理员手动改 cron / 暂停 / 立即执行 / 查 run 历史** 都在 `/admin/scheduled-jobs`。admin 的 cron 或 enabled 改动 **不会** 被后续 integration 更新覆盖（只有 display_name / description 会刷新）。

### 3.5 一键全量同步按钮

`/admin/integrations?kind=cloud` 列表页的"立即同步"按钮现在走一个新端点 `POST /api/v1/cmdb/sync/{id}/trigger-all`：产生 **一条 CmdbSyncJob 覆盖所有 enabled capability**（方便管理员看总览）。这跟每能力的 scheduled_job 独立并行 —— 两者互不影响。

### 3.6 月度账单（新）

| | |
|---|---|
| 表 | `cmdb_billing_monthly` (integration_id × month，total_cost_usd + cost_by_service JSON) |
| Handler | `cmdb.billing_sync`，cron 每月 2 号 2:xx（按 slug hash 分散 minute） |
| Driver 方法 | AWS 的 `get_monthly_billing` 走 Cost Explorer `ce.get_cost_and_usage` |
| UI | `/admin/cloud/billing`：当前月份总额 + 按账号分摊 + Top 服务 + 历史趋势 |
| 预算告警 | `platform_settings` 里 `billing.budget_usd.default` 或 `billing.budget_usd.<slug>`（后者优先）。超预算在 handler 里自动向 Feishu `alert_webhook_url` 推一条。 |

### 3.7 IAM 用户同步（新）

| | |
|---|---|
| 表 | `cmdb_iam_users`（一行 = 云租户里的一个 IAM user：user_name、arn、策略、用户组、MFA、tags） |
| Capability | `iam`；AWS 已实现，Aliyun RAM / 腾讯 CAM 未实现（`CapabilityNotSupported`） |
| 同步频率 | 跟其他资源一样,per-capability scheduled_job `cmdb.sync.<slug>.iam` 每小时一次 |
| UI | `/admin/cloud/iam-users`：按租户筛选、按 MFA / 搜索用户名/ARN、看策略挂载、用户组、创建时间、上次登录 |

### 3.8 AWS Bedrock API Key 自动授权（新）

当员工通过 `/my/requests` 申请 provider = "AWS Bedrock" 的 API Key，走完 Feishu 审批并转为 approved 的那一刻，`ApiKeyService.handle_feishu_callback` 会调用 `maybe_auto_grant_bedrock(db, assignment)`：

1. 解析 AWS SG 云租户：先看 `bedrock.provisioning_integration_slug` 平台参数 override，否则 slug 匹配 `aws-sg` / `aws_sg` / `aws-singapore`，最后再 fallback 到任何 provider=aws + 名字含 sg/singapore 的 enabled 云账号。
2. 解析申请人 `work_email`，构造 IAM 用户名：`bedrock-<local-part>-<request-number 后缀>`（最长 64 字符，合法字符集）。
3. 通过 AWS 驱动的 `create_iam_user_with_policy`：`iam.create_user` → `iam.put_user_policy`（attach inline `bedrock-invoke` policy）→ `iam.create_access_key`。
4. 返回的 AK/SK 以结构化文本写进 `assignment.key_value`（4 行 `KEY=VALUE`），`status` 推到 `active`。员工到 `/my/api-keys` 里点 reveal 就能拿到。
5. 任何一步出错都把错误写到 `assignment.detail.bedrock.error`，让 IT Admin 在 `/admin/api-keys` 里看到红字，可以手动补发 key。

想把授权切到别的 AWS 租户：在 `/admin/settings` 新增 `bedrock.provisioning_integration_slug = "<目标 integration slug>"`。想改策略：编辑 `server/app/domains/apikey/bedrock_autogrant.py` 里的 `_BEDROCK_POLICY`（比如收紧到 Claude 模型家族 ARN）。

### 3.9 查看同步状态（原子 job）

```bash
# 列出某云账号最近同步任务
curl "http://localhost:8000/api/v1/cmdb/sync-jobs?integration_id=$INTEGRATION_ID" \
  -H "Authorization: Bearer <token>"

# 单任务详情
curl http://localhost:8000/api/v1/cmdb/sync-jobs/<job_id> \
  -H "Authorization: Bearer <token>"
```

同步状态：`queued` → `running` → `success` / `failed` / `partial_failure`。菜单入口删了，但 REST API 仍可用（例如外部仪表板脚本可以继续读）。

### 3.10 腾讯云连接器配置（生产级 2026-04-24）

**凭证来源**：腾讯云控制台 → 访问管理 CAM → 访问密钥 → API 密钥管理 → 新建密钥。拿到 SecretId + SecretKey。

**字段**：
- `secret_id` / `secret_key`：必填
- `regions`：CVM / CLB 同步的 Region 列表（逗号分隔）。默认包含所有公有云 Region,可以缩到 `ap-shanghai,ap-beijing,ap-guangzhou` 加快同步
- `account_mode`：`iam`（默认)。腾讯云是单租户 CAM 模式

**能力清单**：
| capability | API | 说明 |
|---|---|---|
| `servers` | CVM `DescribeInstances` | 按 region 拉机器;terminated 过滤 |
| `domains` | Domain `DescribeDomainNameList` | 腾讯云域名注册;没开通返回空 |
| `dns_records` | DNSPod `DescribeDomainList` + `DescribeRecordList` | DNSPod 托管的域名 |
| `load_balancers` | CLB `DescribeLoadBalancers` | 按 region;CLB 分 application / classic |
| `cdn` | CDN `DescribeDomainsConfig` | 全局 |
| `object_storage` | COS v5 `list_buckets` | 全局;通过 `cos-python-sdk-v5` |
| `iam` | CAM `ListUsers` + 每用户 `ListAttachedUserAllPolicies` + `ListGroupsForUser` | 全量 CAM 子账号清单 |
| `user_revoke` | CAM `DetachUserPolicy → DeleteAccessKey → UpdateUser(disable console) → DeleteUser(Force=1)` | 离职删号 |

**权限申请（grant/revoke）**：
- Role catalog 来自 CAM `ListPolicies`(1000+ 条,包含所有 QCloud 预设 + 自定义)
- `role_key = PolicyName`(如 `AdministratorAccess`);driver 自动 resolve 到 PolicyId
- Owner 等价角色：`AdministratorAccess`(可在 platform_setting 里扩)

**月度账单**：`DescribeBillDetail` 按天拉昨天的 bill rows,按 `(day, BusinessCodeName)` client-side 聚合。货币 CNY。**注意**:账户交易条数多时一页 100,`max_pages=50` 的硬上限即 5000 行/天,超过就截断(cron 友好)。

### 3.11 UCloud 连接器配置（生产级 2026-04-24）

**凭证来源**：UCloud 控制台 → UAPI 密钥 → 创建 PublicKey / PrivateKey 对。

**字段**：
- `public_key` / `private_key`：必填(签名是 SHA-1 排序拼接 + PrivateKey)
- `project_id`：空用默认项目;多项目账号填 `org-xxxxxx`
- `regions`：UHost / ULB 同步的 Region 列表
- `dns_zones`：DNS 同步需要的 zone 列表(逗号分隔)。**留空则 DNS 同步 skip**(UCloud 没有 list-all-zones API,必须由 admin 显式列出)

**能力清单**：
| capability | API | 说明 |
|---|---|---|
| `servers` | `DescribeUHostInstance` | 按 region |
| `dns_records` | `UdnrDomainDNSQuery` per zone | **必须配置 `dns_zones`**;未配则 CapabilityNotSupported(scheduled_job 标 skipped) |
| `load_balancers` | `DescribeULB` | 按 region |
| `cdn` | `GetUcdnDomainInfoList` | 全局 |
| `object_storage` | `DescribeBucket` | 全局 |
| `user_revoke` | `DetachPoliciesFromUser → DeleteAccessKey → DeleteUser` | 离职删号 |

**注意 UCloud 不支持**：
- **`domains` 域名注册列举** — UCloud 没有 public list-all-domains API。历史 40 条域名(从旧 CMDB 迁入)原样展示,不做 re-sync。
- **自动列出 DNS zones** — `UdnrDomainDNSQuery` 只能按已知域名查询,必须在 `dns_zones` 手动填。

**权限申请(grant/revoke)**：
- Role catalog = 内置(admin-editable,`cloud.ucloud.default_policies`)+ 租户自定义(live `ListPolicies`)
- `role_key = PolicyName`(如 `AdminPolicy`);driver 自动 resolve 到 PolicyURN
- Owner 等价角色：`AdminPolicy`

**日级账单**：`ListUBillDetail` 按 BillingCycle=YYYY-MM 拉本月所有 bill rows,按 `(day, ProductName)` 聚合。货币 CNY。

---

## 四、CMDB API 速查

| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| GET | `/api/v1/integrations?kind=cloud` | 列出云账号 | it_admin |
| POST | `/api/v1/integrations` | 创建云账号（body 指定 `kind=cloud`，会自动 seed scheduled_jobs） | it_admin |
| PATCH | `/api/v1/integrations/{slug}` | 更新云账号（改 capabilities 会 diff scheduled_jobs） | it_admin |
| DELETE | `/api/v1/integrations/{slug}` | 删除云账号（级联清理 scheduled_jobs） | it_admin |
| POST | `/api/v1/cmdb/sync/{id}/trigger` | 手动按指定 resource_types 触发同步 | it_admin |
| POST | `/api/v1/cmdb/sync/{id}/trigger-all` | 一键按 integration.capabilities 触发全部能力 | it_admin |
| GET | `/api/v1/cmdb/sync-jobs` | 列出原子同步任务（可按 integration_id 过滤） | it_admin |
| GET | `/api/v1/cmdb/sync-jobs/{id}` | 单任务详情 | it_admin |
| GET | `/api/v1/cmdb/iam-users` | 列出 IAM 用户（分页，按 tenant / mfa / search 筛选） | it_admin |
| GET | `/api/v1/cmdb/billing/monthly` | 最近 N 个月账单行 | it_admin |
| GET | `/api/v1/cmdb/billing/summary?month=YYYY-MM` | 多云聚合账单视图 | it_admin |
| GET | `/api/v1/cmdb/servers` | 列出云服务器 | 员工可读（限 owner 范围）/ it_admin 全部 |
| GET | `/api/v1/cmdb/servers/{id}` | 服务器详情 | 同上 |
| GET | `/api/v1/cmdb/domains` | 列出域名 | 同上 |
| GET | `/api/v1/cmdb/domains/{id}` | 域名详情 | 同上 |

> 注：`cloud_admin` 角色在 migration 040 已合并进 `it_admin`。

---

## 五、数据库表结构（2026-04-22 起）

```
integrations             — 统一连接器表（kind='cloud' 即云账号）
cmdb_sync_jobs           — 原子同步任务记录（FK integration_id）
cmdb_servers             — 云服务器（软删除）
cmdb_domains             — 域名（软删除）
cmdb_dns_records         — DNS 记录（软删除）
cmdb_load_balancers      — 负载均衡（软删除）
cmdb_cdn_domains         — CDN 域名（软删除）
cmdb_object_storage      — 对象存储（软删除）
cmdb_iam_users           — 云租户里的 IAM / RAM / CAM 用户（软删除，NEW)
cmdb_billing_monthly     — 月度账单（integration_id + month 唯一键，NEW)
cmdb_user_mapping        — 旧 CMDB 用户名 → Helix person_id 映射
scheduled_jobs           — 所有定时任务
                          handler_key='cmdb.resource_sync' → 每条云账号×能力一行
                          handler_key='cmdb.billing_sync' → 每条云账号一行（月度）
scheduled_job_runs       — 定时任务执行历史（取代了旧 sync-jobs 页面的大部分作用）
```

所有 CMDB 资源表均支持软删除（`deleted_at`），可通过 `WHERE deleted_at IS NULL` 过滤活跃记录。

---

## 六、常见问题

### 同步任务卡在 `queued`

原因：后台任务无法建立到 TiDB Cloud 的新 SSL 连接（dev 环境网络限制）。  
排查：查看 uvicorn 日志，确认是否有 `(2013, 'Lost connection to MySQL server')` 错误。  
生产环境（EC2）不受此影响。

### 凭证解密失败

原因：`CREDENTIAL_ENCRYPTION_KEY` 环境变量与加密时不一致。  
修复：确认 `.env` 中 `CREDENTIAL_ENCRYPTION_KEY` 与建账号时使用的 key 相同。

### 迁移脚本报 `Access denied for user 'cmdb'`

原因：Cloud SQL Proxy 未启动或 `CMDB_DB_PASSWORD` 错误。  
修复：检查 proxy 进程 `ps aux | grep cloud-sql-proxy`，密码见 `cmdb-backend/config/config_prod.yaml`。

### `/admin/cloud/accounts` 404

2026-04-21 起该页面已下线。云账号改在 `/admin/integrations` 里管理。旧菜单项也一并移除。

### `/admin/cloud/sync-jobs` 404

migration 047 起该页面已下线（路由自动 302 到 `/admin/scheduled-jobs`）。每能力同步记录通过 scheduled_job_runs 查看。

### scheduled_job 里没看到我新建的云账号

1. 确认 integration 的 `kind='cloud' AND enabled=true`；
2. 确认 `capabilities` 列表不为空（空表示没开任何能力）；
3. 如果以上都 OK 但仍没出现，检查后端日志是否有 `IntegrationService.create` 里调用 `sync_jobs_for_integration` 的错误；
4. 应急补救：手动调 `POST /api/v1/integrations/{slug}` 做一次 no-op update 触发重新 sync。

### Bedrock 自动授权没执行

1. `/admin/api-keys/<id>` 里看 `detail.bedrock`，会告诉你卡在哪一步（`no-integration` / `decrypt-failed` / `aws-failed` / `in-flight-needs-manual-recovery`）；
2. `no-integration`：检查有没有 enabled 的 AWS 云连接器，slug 是否是 `aws-sg`（或设置 `bedrock.provisioning_integration_slug`）；
3. `aws-failed`：根据 `detail.bedrock.error` 判断，常见是 AK 权限不够（需要 `iam:CreateUser / PutUserPolicy / CreateAccessKey`）。
4. `in-flight-needs-manual-recovery`：上一次 callback 崩在了 AWS 调用之后、Helix 提交之前 —— 为了防止重复创建 IAM 用户，我们留了个 tombstone 拒绝重试。恢复步骤：
    - 去 AWS IAM 控制台（AWS SG）搜 `detail.bedrock.user_name_attempted` 里的用户名；
    - **如果存在**：拿到该用户的 access key（旧的不可读，去 Security credentials → Create access key 生成一把新的），然后手动更新 `api_key_assignments` 行：
      ```sql
      UPDATE api_key_assignments
      SET key_value = 'ACCESS_KEY_ID=AKIA...\nSECRET_ACCESS_KEY=...\nREGION=ap-southeast-1\nUSER_ARN=arn:aws:iam::...',
          status = 'active',
          detail = JSON_SET(JSON_REMOVE(detail, '$.bedrock.in_flight'),
                            '$.bedrock.granted', true,
                            '$.bedrock.iam_user_name', '<username>',
                            '$.bedrock.recovered_manually', true)
      WHERE id = '<assignment_id>';
      ```
    - **如果不存在**（AWS 调用根本没创建成功）：清掉 in_flight 让下次 callback 正常重试：
      ```sql
      UPDATE api_key_assignments
      SET detail = JSON_REMOVE(detail, '$.bedrock.in_flight')
      WHERE id = '<assignment_id>';
      ```
    - 然后在 Feishu 审批上点"再次发送"，或直接调 `POST /api/v1/approvals/feishu/callback`（带正确签名）重放一次审批 approved 事件。

### 月度账单预算告警

- 阈值键：`billing.budget_usd.default`（全局）或 `billing.budget_usd.<integration-slug>`（按账号 override）。默认 seed 1000 USD；按需修改 `/admin/settings`。
- Feishu 机器人 webhook：`feishu.alert_webhook_url` 平台参数，未配置就只落日志。


---

## migration 173 — legacy CMDB 数据迁移 (2026-05-17)

把 GCP 上托管的旧 Go CMDB (`amber-lzz/cmdb-backend` → DB `cmdb` on `34.150.22.222`) 的 **server / domain / dns_record / load_balancer / cdn** 全部迁到 Helix，包含 owner、app、remark、IDC 元数据等。**不**迁 application 模块。

### 同 PR 一起上的功能改动（不需要单独运维）

- 5 张表加 `last_modified_by_person_id` + `last_modified_at`（PATCH / 批量 / DNS enable-disable 写入）
- LB 加 `owner_person_id` FK（一次性 backfill 跟 alembic 一起跑）
- DNS request 流学会 `enable` / `disable` 两个 action；admin 直通走 `POST /cmdb/dns-records/{id}/{enable,disable}`
- DNSPod + Aliyun DNS provider 实现 `set_dns_record_status`；AWS Route 53 没有等价 API，调用会返回 501 + `DNS_PROVIDER_NO_ENABLE_DISABLE`
- AWS Route 53 Domains 缺权限时优雅降级（`AccessDeniedException` 不再 500；写一条 `partial_failures` 到 sync 结果里）。要启用域名注册同步，AWS integration 的 IAM policy 要追加：
  - `route53domains:ListDomains`
  - `route53domains:GetDomainDetail`
  - region 锁死在 `us-east-1`
- `POST /cmdb/servers/batch-annotate`、`/cmdb/load-balancers/batch-annotate`、`/cmdb/dns-records/batch-owner`（admin only）

### Owner 重派规则（migration 173 OwnerResolver）

legacy 表里的 `owner` 字段是邮箱前缀字符串（`shuai.feng` 等），通过 legacy 的 `user` 表二跳到 `email`，再去 Helix `identity_persons.work_email` 找人。然后：

1. 找到人 + active + 不是 Max Liu (`liuqi@pingcap.com`) → 直接落这个人
2. 找到但 `employment_status != 'active'` → 沿 `manager_person_id` 向上递归（最多 10 跳）找第一个 active 且非 Max Liu 的 lead
3. 链路里**任何一层**踩到 Max Liu → 立即兜底到 **孟凡生** (`fansheng.meng@pingcap.com`)
4. 链路断（manager 为 NULL / 全员离职 / 跳数超限）→ 兜底孟凡生
5. legacy 里就没 owner（空串）→ `owner_person_id = NULL`，不兜底（保留"无人认领"状态）
6. legacy `owner` 在 cmdb.user 表里找不到 → 兜底孟凡生

**启动前硬检查**：脚本会先确认两个兜底邮箱在 `identity_persons` 里都有 active 行，否则 abort。如果业务上需要改 fallback owner，改 `scripts/migrate_cmdb.py` 顶部的 `FALLBACK_OWNER_EMAIL` / `BLOCKED_LEAD_EMAIL`。

### 操作顺序（生产）

```bash
# 0. 准备：worktree 里把 .env 链好，确保 DATABASE_URL_SYNC 指向 prod TiDB
cd <helix-worktree>
ln -sf "$(pwd)/.env" server/.env
# 在 .env 里加一行（如果还没有）：
# CMDB_DB_PASSWORD=<legacy DB 密码，见 cmdb-backend-main/config_prod.yaml>

# 1. 起 Cloud SQL Proxy 连 legacy DB
cloud-sql-proxy --gcloud-auth --address 127.0.0.1 --port 3307 \
    gcp-digital-infra:asia-east2:pch-sql-db

# 2. 跑 alembic 到 173（包含 LB owner 一次性 backfill）
cd server
uv run alembic upgrade head
# 验证
uv run alembic current   # 期望: 173 (head)

# 3. preflight（只读）—— 输出 owner 直接匹配覆盖率
uv run python scripts/migrate_cmdb.py --phase preflight

# 4. 正式跑（含 server/domain/dns/lb/cdn 五个 phase + validate）
uv run python scripts/migrate_cmdb.py --phase all

# 5. 单独跑 LB owner 二次回填（如果一次性 backfill 后还有遗漏）
uv run python scripts/migrate_cmdb.py --phase backfill_lb_owners

# 6. (可选) 给 AWS integration 的 IAM policy 加 route53domains 权限，
#    然后触发一次 sync 验证域名拉取
```

### 回滚

- 数据：`migrate_cmdb.py` 用 `ON DUPLICATE KEY UPDATE` + `COALESCE(target.col, VALUES(col))`，所以**任意 phase 可以重跑且不会覆盖管理员后续手工编辑**。
- Schema：`alembic downgrade 172` 会丢掉 173 加的所有列（包括 LB 新 FK），不影响已有 prod 数据，但 UI 列会立即看不到修改人字段。
- AWS Route 53 Domains 降级：没有任何状态需要清，sync 结果里的 `partial_failures` 数组在下次 sync 自然刷新。

### Vendor → Integration 映射（已修正）

旧脚本的 bug：所有 legacy 数据都被塞进 UCloud 兜底租户。修正后：

| legacy `vendor_id` | 目标 integration slug | 说明 |
|---|---|---|
| 1 | `ksyun-legacy` | 金山云 servers / LB |
| 2 | `ucloud-primary` | UCloud servers / LB / 兜底 |
| 4 | `ucloud-primary` | 七牛 CDN — 无 Qiniu integration，兜底 |
| 5 | `tencent-primary` | DNSPod DNS 记录 |
| 6 | `aliyun-primary` | 阿里云域名注册 |
| 9 / 10 | `ucloud-primary` | GoDaddy / Namecheap — 无对应 integration，兜底 |
