Skip to content

Commit 4398bbe

Browse files
authored
Merge pull request #101 from zhytao/feat/cmdb-itop-sync
feat(cmdb): add iTop CMDB bidirectional sync module
2 parents cbb09d8 + b737099 commit 4398bbe

26 files changed

Lines changed: 2340 additions & 23 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,4 @@ backend/backups/
196196
# 十八、Lint 输出转储(Lint output dumps)
197197
# =============================================================================
198198
eslint.txt
199+
.zcode/

backend/src/models/migrations/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ import v058DcConstraints from './v058_dc_constraints';
116116
import v059DcPolymorphicFk from './v059_dc_polymorphic_fk';
117117
// === agent_executions 归档 v060 ===
118118
import v060AgentExecutionsArchive from './v060_agent_executions_archive';
119+
// === CMDB 同步表 v061 ===
120+
import v061CmdbSyncTables from './v061_cmdb_sync_tables';
119121

120122
// Helper: wrap sync up/down into async
121123
function wrapAsync(fn: (db: any) => void): (db: any) => Promise<void> {
@@ -370,6 +372,8 @@ export const ALL_MIGRATIONS: Migration[] = [
370372
v059DcPolymorphicFk,
371373
// v060: agent_executions 归档表 + 复合索引
372374
v060AgentExecutionsArchive,
375+
// v061: CMDB 同步状态与日志表
376+
v061CmdbSyncTables,
373377
];
374378

375379
export function createMigrationManager(db: any): MigrationManager {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import type { Migration } from './migrationFramework';
3+
import { logger } from '../../utils/logger';
4+
5+
/**
6+
* Migration v061 — CMDB 同步状态与日志表
7+
*
8+
* 为 iTop CMDB 双向同步模块提供持久化支持:
9+
* - cmdb_sync_state: 记录每个 CI 类型的同步状态(最近同步时间、计数、错误)
10+
* - cmdb_sync_log: 每次同步操作的详细日志
11+
*/
12+
const v061CmdbSyncTables: Migration = {
13+
id: '20250101000061',
14+
version: 61,
15+
name: 'cmdb_sync_tables',
16+
description: 'CMDB sync state and log tables for iTop integration',
17+
18+
up: async (db: any) => {
19+
logger.info('🔄 Creating CMDB sync tables...');
20+
21+
// 同步状态表 — 每个 CI 类型一行
22+
db.exec(`
23+
CREATE TABLE IF NOT EXISTS cmdb_sync_state (
24+
ci_type TEXT PRIMARY KEY,
25+
direction TEXT DEFAULT 'pull',
26+
last_sync_at TEXT,
27+
last_sync_duration_ms INTEGER,
28+
last_count INTEGER DEFAULT 0,
29+
last_status TEXT DEFAULT 'pending',
30+
last_error TEXT,
31+
itop_id_map TEXT DEFAULT '{}',
32+
updated_at TEXT DEFAULT (datetime('now','localtime'))
33+
);
34+
`);
35+
36+
// 同步日志表 — 每次同步的详细记录
37+
db.exec(`
38+
CREATE TABLE IF NOT EXISTS cmdb_sync_log (
39+
id TEXT PRIMARY KEY,
40+
sync_batch_id TEXT NOT NULL,
41+
timestamp TEXT DEFAULT (datetime('now','localtime')),
42+
direction TEXT NOT NULL,
43+
ci_type TEXT NOT NULL,
44+
action TEXT NOT NULL,
45+
itop_id TEXT,
46+
itop_class TEXT,
47+
platform_id TEXT,
48+
platform_table TEXT,
49+
success INTEGER DEFAULT 1,
50+
message TEXT,
51+
details TEXT
52+
);
53+
54+
CREATE INDEX IF NOT EXISTS idx_cmdb_sync_log_batch ON cmdb_sync_log(sync_batch_id);
55+
CREATE INDEX IF NOT EXISTS idx_cmdb_sync_log_ci_type ON cmdb_sync_log(ci_type, timestamp DESC);
56+
CREATE INDEX IF NOT EXISTS idx_cmdb_sync_log_timestamp ON cmdb_sync_log(timestamp DESC);
57+
`);
58+
59+
logger.info('✅ CMDB sync tables created');
60+
},
61+
62+
down: async (db: any) => {
63+
db.exec(`DROP INDEX IF EXISTS idx_cmdb_sync_log_timestamp;`);
64+
db.exec(`DROP INDEX IF EXISTS idx_cmdb_sync_log_ci_type;`);
65+
db.exec(`DROP INDEX IF EXISTS idx_cmdb_sync_log_batch;`);
66+
db.exec(`DROP TABLE IF EXISTS cmdb_sync_log;`);
67+
db.exec(`DROP TABLE IF EXISTS cmdb_sync_state;`);
68+
},
69+
};
70+
71+
export default v061CmdbSyncTables;

backend/src/modules/_registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import containerRoutes from './containers/routes';
2424
import databaseRoutes from './database/routes';
2525
import dcRoutes from './dc/routes';
2626
import importExportRoutes from './import-export/routes';
27+
import cmdbSyncRoutes from './cmdb-sync/routes';
2728
import infraRoutes from './infra/routes';
2829
import kubernetesRoutes from './kubernetes/routes';
2930
import linkageRoutes from './linkage/routes';
@@ -66,6 +67,7 @@ const modules: ModuleConfig[] = [
6667
{ path: '/api/v1', router: databaseRoutes },
6768
{ path: '/api/v1', router: dcRoutes },
6869
{ path: '/api/v1', router: importExportRoutes },
70+
{ path: '/api/v1', router: cmdbSyncRoutes },
6971
{ path: '/api/v1', router: infraRoutes },
7072
{ path: '/api/v1', router: kubernetesRoutes },
7173
{ path: '/api/v1', router: linkageRoutes },
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# CMDB 同步模块 (`cmdb-sync/`)
2+
3+
> **DDD 限界上下文**:与外部 CMDB 系统(当前支持 iTop)的双向资产同步
4+
> **聚合根**`CmdbSyncState``CmdbSyncLog`
5+
> **最后刷新**:2026-08-07
6+
7+
## 职责
8+
9+
从 iTop CMDB 拉取配置项(CI)到平台资产表,支持定时同步和手动触发。当前实现 **Pull** 方向,字段映射如下:
10+
11+
| iTop CI 类 | 平台表 | 说明 |
12+
|------------|--------|------|
13+
| `Location` | `dc_rooms` | 机房 |
14+
| `Rack` | `dc_racks` | 机柜(依赖 Location 先同步) |
15+
| `Server` | `servers` | 服务器(不同步 SSH 凭证,需手动配置) |
16+
| `DatacenterDevice` | `network_devices` / `dc_pdus` |`finalclass` 分流:pdu/ups→dc_pdus,其余→network_devices |
17+
18+
## 内部结构
19+
20+
```
21+
cmdb-sync/
22+
├── routes/
23+
│ ├── config.ts ← GET/PUT /cmdb-sync/config, POST /cmdb-sync/config/test
24+
│ ├── sync.ts ← POST /cmdb-sync/trigger, GET /cmdb-sync/status, GET /cmdb-sync/logs
25+
│ └── index.ts ← 路由聚合
26+
├── services/
27+
│ ├── itopClient.ts ← iTop REST/JSON API 客户端(core/get、core/create、core/update、core/get_related)
28+
│ ├── itopConfigService.ts ← 配置管理(非密配置→settings 表,密钥→credentials 表)
29+
│ ├── itopSyncService.ts ← 同步编排(策略模式 SyncStrategy<T> + db.transaction 保证原子性)
30+
│ └── cmdbSyncWriter.ts ← 数据写入层(iTop CI → 平台表 upsert)
31+
├── routes.ts # 模块路由入口
32+
├── index.ts
33+
└── README.md
34+
```
35+
36+
## 路由端点(受保护)
37+
38+
| 方法 | 路径 | 权限 | 说明 |
39+
|------|------|------|------|
40+
| GET | `/cmdb-sync/config` | 已登录 | 获取当前配置(token 掩码) |
41+
| PUT | `/cmdb-sync/config` | admin | 保存配置(Zod 校验) |
42+
| POST | `/cmdb-sync/config/test` | admin | 测试连接(支持临时配置) |
43+
| POST | `/cmdb-sync/trigger` | admin/operator | 手动触发一次同步 |
44+
| GET | `/cmdb-sync/status` | 已登录 | 各 CI 类型的同步状态 |
45+
| GET | `/cmdb-sync/logs` | 已登录 | 同步日志(支持 ci_type/direction/batch_id/limit 过滤) |
46+
47+
## 配置项
48+
49+
存储在 `settings` 表(非密)和 `credentials` 表(密钥,AES-256-GCM 加密):
50+
51+
| Key | 存储 | 默认值 | 说明 |
52+
|-----|------|--------|------|
53+
| `ITOP_API_BASE` | settings | - | iTop rest.php 完整 URL |
54+
| `ITOP_AUTH_USER` | settings | `admin` | 认证用户名 |
55+
| `ITOP_SYNC_ENABLED` | settings | `false` | 是否启用定时同步 |
56+
| `ITOP_SYNC_INTERVAL_MINUTES` | settings | `30` | 同步间隔(1~1440 分钟) |
57+
| `ITOP_TIMEOUT_MS` | settings | `30000` | API 超时(毫秒) |
58+
| `itop` (provider) | credentials | - | iTop 登录密码或 Token(加密) |
59+
60+
## 同步语义
61+
62+
- **依赖顺序**`Location → Rack → Server → DatacenterDevice`(Rack 需要关联到已同步的 Location)
63+
- **ID 映射**`cmdb_sync_state.itop_id_map` 存储 `{ itopId: platformId }` 的 JSON,支持增量同步(已映射的走 UPDATE,未映射的走 INSERT)
64+
- **事务**:每个 CI 类型的「数据写入 + idMap 更新」在同一个 `db.transaction` 内,失败时整体回滚
65+
- **幂等**:重复同步不会创建重复记录(依赖 idMap)
66+
- **冲突处理**
67+
- `dc_racks.room_id` 为 NOT NULL,缺机房时记 error 跳过(不 INSERT null)
68+
- `network_devices.ip_address` 为 UNIQUE,冲突时关联到已有设备
69+
- `servers.ip_address` 无 UNIQUE 约束,直接写入
70+
- **日志**:每次同步生成 `batchId`,所有操作写入 `cmdb_sync_log`(保留 30 天)
71+
72+
## 启动时机
73+
74+
`serviceRegistry.ts` 在容器初始化时判断 `ITOP_SYNC_ENABLED === 'true'` 决定是否调用 `itopSyncService.startSync()`。启用后启动 10 秒先跑一次,之后按间隔定时执行。
75+
76+
## 前端
77+
78+
设置页 `frontend/src/modules/settings/pages/settings/ITopSyncSettings.tsx`,作为「系统设置」的一个 tab。
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* cmdb-sync 模块入口
3+
*
4+
* 对接 iTop CMDB,支持:
5+
* - 从 iTop 拉取 Location/Rack/Server/DatacenterDevice 到平台展示
6+
* - 双向同步机房、机柜、服务器、网络设备资产信息
7+
* - 定时同步(可配置间隔)+ 手动触发
8+
*/
9+
10+
export { default as routes } from './routes';
11+
export { itopClient } from './services/itopClient';
12+
export { itopConfigService } from './services/itopConfigService';
13+
export { itopSyncService } from './services/itopSyncService';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* cmdb-sync 模块 — 默认路由导出
3+
*
4+
* 所有路由受 authenticateToken 保护(在 _registry.ts 中注册)。
5+
*/
6+
7+
import cmdbSyncRoutes from './routes/index';
8+
9+
export default cmdbSyncRoutes;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* iTop CMDB 同步 — 配置管理路由
3+
* GET /cmdb-sync/config 获取当前配置
4+
* PUT /cmdb-sync/config 保存配置(admin)
5+
* POST /cmdb-sync/config/test 测试 iTop 连接(admin)
6+
*/
7+
8+
import { Router, type Request, type Response } from 'express';
9+
import { z } from 'zod';
10+
import { requireRole } from '../../../middleware/auth';
11+
import { validateBody } from '../../../middleware/validation';
12+
import { getErrorMessage } from '../../../utils/errorHelpers';
13+
import { logger } from '../../../utils/logger';
14+
import { itopConfigService } from '../services/itopConfigService';
15+
16+
const router = Router();
17+
18+
// PUT /cmdb-sync/config 入参校验
19+
const saveConfigSchema = z.object({
20+
apiBase: z.string().url('API 地址格式不正确').optional().or(z.literal('')),
21+
authUser: z.string().max(64).optional(),
22+
authToken: z.string().max(512).optional(),
23+
syncEnabled: z.boolean().optional(),
24+
syncIntervalMinutes: z.number().int().min(1).max(1440).optional(),
25+
timeoutMs: z.number().int().min(1000).max(300000).optional(),
26+
sslVerify: z.boolean().optional(),
27+
});
28+
29+
// POST /cmdb-sync/config/test 入参校验(全部可选,允许用已存配置测试)
30+
const testConnectionSchema = z.object({
31+
apiBase: z.string().optional(),
32+
authUser: z.string().optional(),
33+
authToken: z.string().optional(),
34+
sslVerify: z.boolean().optional(),
35+
});
36+
37+
// GET /cmdb-sync/config — 获取当前 iTop 配置
38+
router.get('/config', (_req: Request, res: Response) => {
39+
try {
40+
const config = itopConfigService.getConfig();
41+
res.json({ success: true, data: config });
42+
} catch (error) {
43+
logger.error('Failed to get iTop config:', error as Error);
44+
res.status(500).json({ success: false, message: getErrorMessage(error) });
45+
}
46+
});
47+
48+
// PUT /cmdb-sync/config — 保存 iTop 配置
49+
router.put(
50+
'/config',
51+
requireRole('admin'),
52+
validateBody(saveConfigSchema),
53+
(req: Request, res: Response) => {
54+
try {
55+
const saved = itopConfigService.saveConfig(req.body);
56+
res.json({ success: true, data: saved, message: '配置已保存' });
57+
} catch (error) {
58+
logger.error('Failed to save iTop config:', error as Error);
59+
res.status(500).json({ success: false, message: getErrorMessage(error) });
60+
}
61+
},
62+
);
63+
64+
// POST /cmdb-sync/config/test — 测试 iTop 连接
65+
router.post(
66+
'/config/test',
67+
requireRole('admin'),
68+
validateBody(testConnectionSchema),
69+
async (req: Request, res: Response) => {
70+
try {
71+
// 支持用 body 传入临时配置测试,也支持用已存储配置测试
72+
const result = await itopConfigService.testConnection(req.body);
73+
res.json({ success: result.success, data: result });
74+
} catch (error) {
75+
logger.error('Failed to test iTop connection:', error as Error);
76+
res.status(500).json({ success: false, message: getErrorMessage(error) });
77+
}
78+
},
79+
);
80+
81+
export default router;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* cmdb-sync 模块路由聚合
3+
*/
4+
5+
import { Router } from 'express';
6+
import configRoutes from './config';
7+
import syncRoutes from './sync';
8+
9+
const router = Router();
10+
11+
router.use('/cmdb-sync', configRoutes);
12+
router.use('/cmdb-sync', syncRoutes);
13+
14+
// 健康检查
15+
router.get('/cmdb-sync/health', (_req, res) => {
16+
res.json({ success: true, message: 'CMDB sync routes OK' });
17+
});
18+
19+
export default router;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* iTop CMDB 同步 — 同步操作路由
3+
* POST /cmdb-sync/trigger 手动触发一次同步
4+
* GET /cmdb-sync/status 获取各 CI 类型的同步状态
5+
* GET /cmdb-sync/logs 查看同步日志
6+
*/
7+
8+
import { Router, type Request, type Response } from 'express';
9+
import { z } from 'zod';
10+
import { requireRole } from '../../../middleware/auth';
11+
import { validateQuery } from '../../../middleware/validation';
12+
import { getErrorMessage } from '../../../utils/errorHelpers';
13+
import { logger } from '../../../utils/logger';
14+
import { cmdbSyncStateRepo, cmdbSyncLogRepo } from '../../../repositories';
15+
import { itopSyncService } from '../services/itopSyncService';
16+
17+
const router = Router();
18+
19+
// POST /cmdb-sync/trigger — 手动触发一次同步
20+
router.post('/trigger', requireRole('admin', 'operator'), async (_req: Request, res: Response) => {
21+
try {
22+
// 异步触发,不阻塞请求
23+
const result = await itopSyncService.syncAll();
24+
res.json({ success: result.success, data: result, message: result.message });
25+
} catch (error) {
26+
logger.error('CMDB sync trigger failed:', error as Error);
27+
res.status(500).json({ success: false, message: getErrorMessage(error) });
28+
}
29+
});
30+
31+
// GET /cmdb-sync/status — 获取同步状态
32+
router.get('/status', (_req: Request, res: Response) => {
33+
try {
34+
const states = cmdbSyncStateRepo.listAll();
35+
const syncing = itopSyncService.isSyncing();
36+
res.json({ success: true, data: { states, syncing } });
37+
} catch (error) {
38+
logger.error('Failed to get CMDB sync status:', error as Error);
39+
res.status(500).json({ success: false, message: getErrorMessage(error) });
40+
}
41+
});
42+
43+
// GET /cmdb-sync/logs — 查看同步日志
44+
const logsQuerySchema = z.object({
45+
limit: z.coerce.number().int().min(1).max(500).optional(),
46+
ci_type: z.string().max(64).optional(),
47+
direction: z.enum(['pull', 'push']).optional(),
48+
batch_id: z.string().max(64).optional(),
49+
});
50+
51+
router.get('/logs', validateQuery(logsQuerySchema), (req: Request, res: Response) => {
52+
try {
53+
const limit = req.query.limit ? parseInt(String(req.query.limit), 10) : 50;
54+
const ciType = req.query.ci_type as string | undefined;
55+
const direction = req.query.direction as string | undefined;
56+
const batchId = req.query.batch_id as string | undefined;
57+
58+
const logs = cmdbSyncLogRepo.list({ ci_type: ciType, direction, batch_id: batchId, limit });
59+
res.json({ success: true, data: logs });
60+
} catch (error) {
61+
logger.error('Failed to get CMDB sync logs:', error as Error);
62+
res.status(500).json({ success: false, message: getErrorMessage(error) });
63+
}
64+
});
65+
66+
export default router;

0 commit comments

Comments
 (0)