assets-ai/scripts/import-servers.ts

304 lines
11 KiB
TypeScript

/**
* 导入服务器资产数据
* 用法: npx tsx scripts/import-servers.ts [excel文件路径]
* 默认路径: templates-docs/服务器信息-issue.xlsx
*/
import Database from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
// 动态导入 xlsx
const XLSX = require('xlsx')
const dbPath = process.env.DATABASE_PATH || './data/assets.db'
const dbDir = path.dirname(dbPath)
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true })
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')
// 初始化数据库
function initDb() {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL DEFAULT 'viewer',
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
permissions TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
key_hash TEXT NOT NULL,
permissions TEXT NOT NULL DEFAULT '["assets:read"]',
last_used_at TEXT,
expires_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS assets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
serial_number TEXT UNIQUE,
device_type TEXT NOT NULL,
device_purpose TEXT,
room TEXT,
rack_position TEXT,
node_name TEXT,
business_ip TEXT,
hdm_ip TEXT,
manufacturer TEXT,
device_model TEXT,
status TEXT NOT NULL DEFAULT '在用',
warranty_date TEXT,
cpu_model TEXT, cpu_generation TEXT, cpu_cores INTEGER,
cpu_count INTEGER, cpu_threads INTEGER, cpu_spec TEXT,
memory_model TEXT, memory_frequency TEXT, memory_unit_capacity TEXT,
memory_count INTEGER, memory_total TEXT,
gpu_model TEXT, gpu_power TEXT, gpu_count INTEGER,
nic1_model TEXT, nic1_type TEXT, nic1_speed TEXT, nic1_count INTEGER,
nic2_model TEXT, nic2_type TEXT, nic2_speed TEXT, nic2_count INTEGER,
nic3_model TEXT, nic3_type TEXT, nic3_speed TEXT, nic3_count INTEGER,
sys_disk_model TEXT, sys_disk_spec TEXT, sys_disk_capacity TEXT,
sys_disk_type TEXT, sys_disk_protocol TEXT, sys_disk_speed TEXT, sys_disk_count INTEGER,
data_disk1_model TEXT, data_disk1_spec TEXT, data_disk1_capacity TEXT,
data_disk1_type TEXT, data_disk1_protocol TEXT, data_disk1_speed TEXT, data_disk1_count INTEGER,
data_disk2_model TEXT, data_disk2_spec TEXT, data_disk2_capacity TEXT,
data_disk2_type TEXT, data_disk2_protocol TEXT, data_disk2_speed TEXT, data_disk2_count INTEGER,
data_disk_total_space TEXT,
raid_model TEXT, raid_spec TEXT, raid_count INTEGER,
psu1_model TEXT, psu1_power TEXT, psu1_count INTEGER,
psu2_model TEXT, psu2_power TEXT, psu2_count INTEGER,
psu_total_power TEXT,
board_model TEXT, board_count INTEGER,
raw_data TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
api_key_id INTEGER REFERENCES api_keys(id),
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id INTEGER,
details TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_assets_node_name ON assets(node_name);
CREATE INDEX IF NOT EXISTS idx_assets_business_ip ON assets(business_ip);
CREATE INDEX IF NOT EXISTS idx_assets_device_type ON assets(device_type);
CREATE INDEX IF NOT EXISTS idx_assets_status ON assets(status);
`)
// 创建默认 admin
const bcrypt = require('bcryptjs')
const existingAdmin = db.prepare('SELECT id FROM users WHERE username = ?').get('admin')
if (!existingAdmin) {
const passwordHash = bcrypt.hashSync('admin123', 10)
db.prepare('INSERT INTO users (username, password_hash, display_name, role) VALUES (?, ?, ?, ?)')
.run('admin', passwordHash, '管理员', 'admin')
}
const defaultRoles = [
{ name: 'admin', display_name: '管理员', permissions: '["*"]' },
{ name: 'editor', display_name: '编辑者', permissions: '["assets:read","assets:write","assets:delete"]' },
{ name: 'viewer', display_name: '查看者', permissions: '["assets:read"]' },
]
for (const role of defaultRoles) {
const existing = db.prepare('SELECT id FROM roles WHERE name = ?').get(role.name)
if (!existing) {
db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)')
.run(role.name, role.display_name, role.permissions)
}
}
}
// Excel 列名到数据库字段的映射
const COL_MAP: Record<string, string> = {
'序号': '_skip',
'机房': 'room',
'机架位置': 'rack_position',
'节点名称': 'node_name',
'设备型号': 'device_model',
'序列号': 'serial_number',
'状态': 'status',
'HDM地址\n掩码255.255.255.128\n网关29.237.252.126/25': 'hdm_ip',
'业务管理IP\n掩码255.255.255.128\n网关29.237.253.126/25': 'business_ip',
'维保截止日期': 'warranty_date',
'CPU型号': 'cpu_model',
'CPU代数': 'cpu_generation',
'CPU核心数': 'cpu_cores',
'CPU数量': 'cpu_count',
'CPU总线程数': 'cpu_threads',
'CPU规格': 'cpu_spec',
'内存型号': 'memory_model',
'最大频率': 'memory_frequency',
'单条内存容量': 'memory_unit_capacity',
'数量': 'memory_count',
'内存总量': 'memory_total',
'系统盘型号': 'sys_disk_model',
'系统盘规格': 'sys_disk_spec',
'介质类型': 'sys_disk_type',
'协议': 'sys_disk_protocol',
'最大速率': 'sys_disk_speed',
'数量.1': 'sys_disk_count',
'数据盘型号': 'data_disk1_model',
'数据盘规格': 'data_disk1_spec',
'介质类型.1': 'data_disk1_type',
'协议.1': 'data_disk1_protocol',
'最大速率.1': 'data_disk1_speed',
'数量.2': 'data_disk1_count',
'数据盘总空间': 'data_disk_total_space',
'GPU卡型号': 'gpu_model',
'功率': 'gpu_power',
'数量.3': 'gpu_count',
'RAID卡型号': 'raid_model',
'芯片型号': 'raid_spec',
'数量.4': 'raid_count',
'主板型号': 'board_model',
'网卡1': 'nic1_model',
'数量.5': 'nic1_count',
'芯片型号.1': 'nic1_type',
'端口类型': 'nic1_speed',
'端口数量': '_skip',
'端口总数': '_skip',
'网卡2': 'nic2_model',
'数量.6': 'nic2_count',
'芯片型号.2': 'nic2_type',
'端口类型.1': 'nic2_speed',
'端口数量.1': '_skip',
'端口总数.1': '_skip',
'网卡3': 'nic3_model',
'数量.7': 'nic3_count',
'芯片型号.3': 'nic3_type',
'端口类型.2': 'nic3_speed',
'端口数量.2': '_skip',
'端口总数.2': '_skip',
'电源型号1': 'psu1_model',
'额定功率': 'psu1_power',
'数量.8': 'psu1_count',
'电源型号2': 'psu2_model',
'额定功率.1': 'psu2_power',
'数量.9': 'psu2_count',
'电源总功率': 'psu_total_power',
}
function cleanValue(val: any): string | number | null {
if (val === null || val === undefined || val === '' || (typeof val === 'string' && val.trim() === '')) return null
if (typeof val === 'number') return val
return String(val).trim()
}
function importSheet(ws: any, deviceType: string, devicePurpose: string) {
const data = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' })
if (data.length < 2) {
console.log(` Sheet has no data rows`)
return 0
}
const headers = data[0] as string[]
const rows = data.slice(1)
// Map column index to db field
const fieldMap: { idx: number; field: string }[] = []
for (let i = 0; i < headers.length; i++) {
const header = headers[i]
const field = COL_MAP[header]
if (field && field !== '_skip') {
fieldMap.push({ idx: i, field })
}
}
// 按位置兜底:如果关键字段尚未映射(列名匹配失败时),按固定列索引补上
const hasField = (f: string) => fieldMap.some(m => m.field === f)
if (!hasField('business_ip') && headers.length > 8) {
fieldMap.push({ idx: 8, field: 'business_ip' })
console.log(' 警告: business_ip 未通过列名匹配,按列索引 8 兜底')
}
if (!hasField('hdm_ip') && headers.length > 7) {
fieldMap.push({ idx: 7, field: 'hdm_ip' })
console.log(' 警告: hdm_ip 未通过列名匹配,按列索引 7 兜底')
}
const insertFields = ['device_type', 'device_purpose', ...fieldMap.map(f => f.field)]
const placeholders = insertFields.map(() => '?').join(',')
const stmt = db.prepare(`
INSERT OR REPLACE INTO assets (${insertFields.join(',')}, raw_data)
VALUES (${placeholders}, ?)
`)
let count = 0
const importTime = new Date().toISOString()
const transaction = db.transaction(() => {
for (const row of rows) {
const r = row as any[]
if (!r || r.length === 0 || !r[0]) continue
const values: any[] = [deviceType, devicePurpose]
for (const fm of fieldMap) {
values.push(cleanValue(r[fm.idx]))
}
values.push(JSON.stringify({ imported_at: importTime, raw_row: r }))
stmt.run(...values)
count++
}
})
transaction()
return count
}
async function main() {
const excelPath = process.argv[2] || path.join(__dirname, '..', 'templates-docs', '服务器信息-issue.xlsx')
if (!fs.existsSync(excelPath)) {
console.error(`Excel file not found: ${excelPath}`)
process.exit(1)
}
console.log('Initializing database...')
initDb()
console.log(`Reading Excel: ${excelPath}`)
const wb = XLSX.readFile(excelPath)
console.log('Importing GPU servers...')
const gpuSheet = wb.Sheets['GPU服务器']
const gpuCount = importSheet(gpuSheet, 'GPU服务器', 'GPU计算')
console.log(` Imported ${gpuCount} GPU servers`)
console.log('Importing storage servers...')
const storSheet = wb.Sheets['存储服务器']
const storCount = importSheet(storSheet, '存储服务器', '存储')
console.log(` Imported ${storCount} storage servers`)
console.log(`\nTotal: ${gpuCount + storCount} servers imported`)
db.close()
}
main().catch(console.error)