4c1dc4082e
Build and Push Images / build-and-push (push) Has been cancelled
이전 세션들에서 작업된 아래 범위를 모두 포함: Fleet 서브시스템 (src/fleet/) - fleetDeviceService / fleetCommandService / fleetDeploymentService / fleetReleaseService - fleetMetricsService, fleetScriptService, fleetEdgeConfigService - Edge 디바이스 관리, 커맨드 발행, 배포/릴리스, 스크립트 동기화 Collector 확장 - centralMqttForwarder / centralForwarderConfigService - equipmentStateService, pythonHookRunner, scriptCache - Modbus/OPC-UA/S7/XGT 프로토콜 클라이언트 - targetDbIntrospection (저장 DB 조회) Routes / API - automationDashboardRoutes, centralForwarderRoutes, equipmentStateRoutes DB - importEdgeConfig (Python cached config → Pipeline DB) - seedDataSources (external_db_connections 초기 시드) 엣지 배포 리소스 - docker/edge/Dockerfile.backend.prod, Dockerfile.frontend.prod - docker/edge/docker-compose.edge.yml 프론트엔드 - admin/automaticMng (centralForwarder, dashboard, equipmentState) - admin/fleet (commands, devices, deployments, releases, scripts, alerts) - admin/pipeline-device 개선 (저장 DB 드롭다운, 태그 매핑 등) - ExternalDbConnectionModal, ScriptsManagerDialog 등 신규 컴포넌트 - lib/api: automationDashboard, centralForwarder, equipmentState, fleet docs/ - EDGE_SERVER_STRUCTURE, FLEET_COMPLETE, FLEET_EDGE_INTEGRATION, FLEET_HOOK_INTEGRATION Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
207 lines
5.5 KiB
TypeScript
207 lines
5.5 KiB
TypeScript
/**
|
|
* Fleet Command Service
|
|
* - 원격 커맨드 실행 (MQTT 기반)
|
|
* - 9가지 커맨드 타입 지원
|
|
*/
|
|
|
|
import { query, queryOne } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
import { getFleetMqttBroker } from "./mqttBroker";
|
|
|
|
export type CommandType =
|
|
| "restart_container"
|
|
| "pull_image"
|
|
| "update_agent"
|
|
| "health_check"
|
|
| "execute_script"
|
|
| "deploy"
|
|
| "rollback"
|
|
| "collect_logs"
|
|
| "restart_device";
|
|
|
|
export type CommandStatus = "pending" | "sent" | "executing" | "success" | "failed" | "timeout";
|
|
|
|
export interface FleetCommand {
|
|
id?: number;
|
|
device_id: string;
|
|
command_type: CommandType;
|
|
payload?: Record<string, any>;
|
|
status?: CommandStatus;
|
|
result?: Record<string, any>;
|
|
error_message?: string;
|
|
issued_by?: string;
|
|
issued_at?: Date;
|
|
sent_at?: Date;
|
|
responded_at?: Date;
|
|
timeout_sec?: number;
|
|
}
|
|
|
|
export class FleetCommandService {
|
|
/**
|
|
* 커맨드 발행 (DB 저장 + MQTT 전송)
|
|
*/
|
|
static async issueCommand(
|
|
deviceId: string,
|
|
commandType: CommandType,
|
|
payload: Record<string, any> = {},
|
|
issuedBy?: string,
|
|
timeoutSec = 300,
|
|
): Promise<FleetCommand> {
|
|
// 커맨드 타입 검증
|
|
const typeCheck = await queryOne<any>(
|
|
`SELECT command_type FROM fleet_command_types WHERE command_type = $1`,
|
|
[commandType],
|
|
);
|
|
if (!typeCheck) {
|
|
throw new Error(`알 수 없는 커맨드 타입: ${commandType}`);
|
|
}
|
|
|
|
// 디바이스 확인
|
|
const device = await queryOne<any>(
|
|
`SELECT device_id, is_online FROM fleet_devices WHERE device_id = $1`,
|
|
[deviceId],
|
|
);
|
|
if (!device) {
|
|
throw new Error(`존재하지 않는 디바이스: ${deviceId}`);
|
|
}
|
|
|
|
// DB에 커맨드 기록
|
|
const result = await query<FleetCommand>(
|
|
`INSERT INTO fleet_commands
|
|
(device_id, command_type, payload, status, issued_by, timeout_sec)
|
|
VALUES ($1, $2, $3::jsonb, 'pending', $4, $5)
|
|
RETURNING *`,
|
|
[deviceId, commandType, JSON.stringify(payload), issuedBy || null, timeoutSec],
|
|
);
|
|
const command = result[0];
|
|
|
|
// MQTT로 디바이스에 전송
|
|
try {
|
|
const broker = getFleetMqttBroker();
|
|
await broker.sendCommandToDevice(deviceId, {
|
|
command_id: command.id,
|
|
command_type: commandType,
|
|
payload,
|
|
timeout_sec: timeoutSec,
|
|
});
|
|
|
|
await query(
|
|
`UPDATE fleet_commands SET status = 'sent', sent_at = NOW() WHERE id = $1`,
|
|
[command.id],
|
|
);
|
|
command.status = "sent";
|
|
logger.info(`[Fleet] 커맨드 발송: ${commandType} → ${deviceId} (id=${command.id})`);
|
|
} catch (e: any) {
|
|
await query(
|
|
`UPDATE fleet_commands SET status = 'failed', error_message = $2 WHERE id = $1`,
|
|
[command.id, `MQTT 발송 실패: ${e.message}`],
|
|
);
|
|
command.status = "failed";
|
|
command.error_message = e.message;
|
|
logger.error(`[Fleet] 커맨드 MQTT 전송 실패 (id=${command.id}):`, e);
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
/**
|
|
* 디바이스 응답 수신 처리 (MQTT 구독자에서 호출)
|
|
*/
|
|
static async handleResponse(
|
|
deviceId: string,
|
|
response: {
|
|
command_id: number;
|
|
status: "success" | "failed" | "executing";
|
|
result?: Record<string, any>;
|
|
error?: string;
|
|
},
|
|
) {
|
|
const newStatus: CommandStatus =
|
|
response.status === "success"
|
|
? "success"
|
|
: response.status === "failed"
|
|
? "failed"
|
|
: "executing";
|
|
|
|
await query(
|
|
`UPDATE fleet_commands SET
|
|
status = $1,
|
|
result = $2::jsonb,
|
|
error_message = $3,
|
|
responded_at = NOW()
|
|
WHERE id = $4 AND device_id = $5`,
|
|
[
|
|
newStatus,
|
|
JSON.stringify(response.result || {}),
|
|
response.error || null,
|
|
response.command_id,
|
|
deviceId,
|
|
],
|
|
);
|
|
logger.info(
|
|
`[Fleet] 커맨드 응답: id=${response.command_id} device=${deviceId} status=${newStatus}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 커맨드 목록 조회
|
|
*/
|
|
static async listCommands(filter: {
|
|
device_id?: string;
|
|
command_type?: string;
|
|
status?: string;
|
|
limit?: number;
|
|
} = {}) {
|
|
const wheres: string[] = [];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (filter.device_id) {
|
|
wheres.push(`device_id = $${idx++}`);
|
|
params.push(filter.device_id);
|
|
}
|
|
if (filter.command_type) {
|
|
wheres.push(`command_type = $${idx++}`);
|
|
params.push(filter.command_type);
|
|
}
|
|
if (filter.status) {
|
|
wheres.push(`status = $${idx++}`);
|
|
params.push(filter.status);
|
|
}
|
|
|
|
const whereClause = wheres.length > 0 ? `WHERE ${wheres.join(" AND ")}` : "";
|
|
params.push(filter.limit || 100);
|
|
|
|
return await query<any>(
|
|
`SELECT * FROM fleet_commands ${whereClause}
|
|
ORDER BY issued_at DESC LIMIT $${idx}`,
|
|
params,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 타임아웃된 커맨드 정리 (주기적으로 호출)
|
|
*/
|
|
static async markTimedOutCommands() {
|
|
const result = await query<any>(
|
|
`UPDATE fleet_commands
|
|
SET status = 'timeout',
|
|
error_message = '응답 타임아웃'
|
|
WHERE status IN ('sent', 'executing')
|
|
AND sent_at IS NOT NULL
|
|
AND sent_at < NOW() - (timeout_sec || ' seconds')::INTERVAL
|
|
RETURNING id, device_id, command_type`,
|
|
);
|
|
if (result.length > 0) {
|
|
logger.warn(`[Fleet] 타임아웃 커맨드: ${result.length}건`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static async getCommandTypes() {
|
|
return await query<any>(
|
|
`SELECT * FROM fleet_command_types ORDER BY category, command_type`,
|
|
);
|
|
}
|
|
}
|