G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
625 lines
22 KiB
Python
625 lines
22 KiB
Python
"""
|
|
CMDB: Institution + Server 관리 + C-1 CI 형상 관리 확장
|
|
|
|
엔드포인트:
|
|
기존: /api/cmdb/institutions, /api/cmdb/servers
|
|
C-1:
|
|
POST /api/cmdb/ci — CI 생성
|
|
GET /api/cmdb/ci — CI 목록
|
|
GET /api/cmdb/ci/{ci_id} — CI 상세 + 관계 + 변경 이력
|
|
PATCH /api/cmdb/ci/{ci_id} — CI 수정
|
|
DELETE /api/cmdb/ci/{ci_id} — CI 폐기(RETIRED 처리)
|
|
POST /api/cmdb/ci/relations — 관계 추가
|
|
DELETE /api/cmdb/ci/relations/{id} — 관계 삭제
|
|
GET /api/cmdb/ci/{ci_id}/history — 변경 이력
|
|
GET /api/cmdb/ci/stats — CI 통계
|
|
POST /api/cmdb/ci/import-servers — 기존 서버 → CI 일괄 등록
|
|
"""
|
|
from datetime import datetime, date
|
|
from typing import Dict, List, Optional
|
|
|
|
import json
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import select, desc, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from middleware.license_guard import check_server_limit
|
|
from models import (
|
|
Institution, InstitutionOut,
|
|
Server, ServerOut, ServerUpdate,
|
|
User, UserRole,
|
|
ConfigItem, ConfigItemOut, ConfigItemCreate, ConfigItemUpdate,
|
|
CIRelation, CIRelationOut, CIRelationCreate,
|
|
CIChangeLog, CIChangeLogOut,
|
|
CIChangeType,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/cmdb", tags=["cmdb"])
|
|
|
|
|
|
@router.get("/institutions", response_model=List[InstitutionOut])
|
|
async def list_institutions(db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user)):
|
|
result = await db.execute(select(Institution).order_by(Institution.inst_name))
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/institutions/{inst_code}/servers", response_model=List[ServerOut])
|
|
async def list_servers(inst_code: str, db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user)):
|
|
r = await db.execute(
|
|
select(Institution).where(Institution.inst_code == inst_code)
|
|
)
|
|
inst = r.scalars().first()
|
|
if not inst:
|
|
raise HTTPException(404, detail="기관을 찾을 수 없습니다.")
|
|
|
|
result = await db.execute(
|
|
select(Server).where(Server.inst_id == inst.id, Server.is_active == True)
|
|
.order_by(Server.server_role, Server.server_name)
|
|
)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/servers", response_model=List[ServerOut])
|
|
async def list_all_servers(
|
|
role: Optional[str] = Query(None),
|
|
inst_code: Optional[str] = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
q = select(Server).where(Server.is_active == True)
|
|
if role:
|
|
q = q.where(Server.server_role == role)
|
|
if inst_code:
|
|
r = await db.execute(select(Institution).where(Institution.inst_code == inst_code))
|
|
inst = r.scalars().first()
|
|
if inst:
|
|
q = q.where(Server.inst_id == inst.id)
|
|
q = q.order_by(Server.server_role, Server.server_name)
|
|
result = await db.execute(q)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/servers/{server_id}", response_model=ServerOut)
|
|
async def get_server(
|
|
server_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
r = await db.execute(select(Server).where(Server.id == server_id))
|
|
server = r.scalars().first()
|
|
if not server:
|
|
raise HTTPException(404, "서버를 찾을 수 없습니다.")
|
|
return server
|
|
|
|
|
|
@router.patch("/servers/{server_id}", response_model=ServerOut)
|
|
async def update_server(
|
|
server_id: int,
|
|
payload: ServerUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.PM, UserRole.ENGINEER):
|
|
raise HTTPException(403, "권한이 없습니다.")
|
|
r = await db.execute(select(Server).where(Server.id == server_id))
|
|
server = r.scalars().first()
|
|
if not server:
|
|
raise HTTPException(404, "서버를 찾을 수 없습니다.")
|
|
for k, v in payload.model_dump(exclude_unset=True).items():
|
|
setattr(server, k, v)
|
|
server.updated_at = datetime.now()
|
|
await db.commit()
|
|
await db.refresh(server)
|
|
return server
|
|
|
|
|
|
@router.post("/servers", response_model=ServerOut, status_code=201)
|
|
async def create_server(
|
|
payload: dict,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
_: None = Depends(check_server_limit),
|
|
):
|
|
"""새 서버 자산 등록."""
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.PM):
|
|
raise HTTPException(403, "ADMIN 또는 PM 권한이 필요합니다.")
|
|
# inst_code → inst_id 변환
|
|
inst_code = payload.pop("inst_code", None)
|
|
inst_id = None
|
|
if inst_code:
|
|
r = await db.execute(
|
|
select(Institution).where(Institution.inst_code == inst_code)
|
|
)
|
|
inst = r.scalars().first()
|
|
if inst:
|
|
inst_id = inst.id
|
|
# sensitive fields: if ip_addr/ssh_user/os_pw_enc provided, store them (encryption handled elsewhere)
|
|
server = Server(inst_id=inst_id, **{k: v for k, v in payload.items()
|
|
if hasattr(Server, k)})
|
|
db.add(server)
|
|
await db.commit()
|
|
await db.refresh(server)
|
|
return server
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
# C-1: CI 형상 관리 (Configuration Item Management)
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
async def _next_ci_id(db: AsyncSession) -> str:
|
|
"""CI-YYYYMMDD-NNNN 형식 CI ID 생성."""
|
|
today = datetime.utcnow().strftime("%Y%m%d")
|
|
prefix = f"CI-{today}-"
|
|
last = (await db.execute(
|
|
select(ConfigItem.ci_id)
|
|
.where(ConfigItem.ci_id.like(f"{prefix}%"))
|
|
.order_by(desc(ConfigItem.ci_id))
|
|
.limit(1)
|
|
)).scalar()
|
|
seq = 1
|
|
if last:
|
|
try:
|
|
seq = int(last.split("-")[-1]) + 1
|
|
except ValueError:
|
|
seq = 1
|
|
return f"{prefix}{seq:04d}"
|
|
|
|
|
|
async def _log_ci_change(
|
|
db: AsyncSession,
|
|
ci: ConfigItem,
|
|
change_type: str,
|
|
changed_by: str,
|
|
field_name: Optional[str] = None,
|
|
old_value: Optional[str] = None,
|
|
new_value: Optional[str] = None,
|
|
sr_id: Optional[str] = None,
|
|
note: Optional[str] = None,
|
|
):
|
|
"""CI 변경 이력 기록."""
|
|
log = CIChangeLog(
|
|
ci_id_fk = ci.id,
|
|
ci_id_str = ci.ci_id,
|
|
change_type = change_type,
|
|
field_name = field_name,
|
|
old_value = old_value,
|
|
new_value = new_value,
|
|
changed_by = changed_by,
|
|
sr_id = sr_id,
|
|
note = note,
|
|
)
|
|
db.add(log)
|
|
|
|
|
|
# ── CI CRUD ───────────────────────────────────────────────────────────────────
|
|
|
|
@router.post("/ci", response_model=ConfigItemOut, status_code=201)
|
|
async def create_ci(
|
|
body: ConfigItemCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""새 형상 항목(CI) 등록."""
|
|
ci_id = await _next_ci_id(db)
|
|
ci = ConfigItem(
|
|
ci_id = ci_id,
|
|
name = body.name,
|
|
ci_type = body.ci_type.upper(),
|
|
category = body.category,
|
|
status = body.status.upper(),
|
|
version = body.version,
|
|
owner = body.owner,
|
|
location = body.location,
|
|
vendor = body.vendor,
|
|
model = body.model,
|
|
serial_number = body.serial_number,
|
|
install_date = body.install_date,
|
|
retire_date = body.retire_date,
|
|
linked_server_id = body.linked_server_id,
|
|
inst_id = body.inst_id,
|
|
sr_id = body.sr_id,
|
|
attributes_json = json.dumps(body.attributes, ensure_ascii=False) if body.attributes else None,
|
|
description = body.description,
|
|
created_by = current_user.username,
|
|
)
|
|
db.add(ci)
|
|
await db.flush()
|
|
await _log_ci_change(
|
|
db, ci,
|
|
change_type = CIChangeType.CREATE.value,
|
|
changed_by = current_user.username,
|
|
new_value = body.name,
|
|
note = "CI 신규 등록",
|
|
)
|
|
await db.commit()
|
|
await db.refresh(ci)
|
|
return ci
|
|
|
|
|
|
@router.get("/ci", response_model=List[ConfigItemOut])
|
|
async def list_ci(
|
|
ci_type: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
inst_id: Optional[int] = Query(None),
|
|
keyword: Optional[str] = Query(None),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
"""CI 목록 조회."""
|
|
conditions = []
|
|
if ci_type:
|
|
conditions.append(ConfigItem.ci_type == ci_type.upper())
|
|
if status:
|
|
conditions.append(ConfigItem.status == status.upper())
|
|
if inst_id:
|
|
conditions.append(ConfigItem.inst_id == inst_id)
|
|
if keyword:
|
|
conditions.append(ConfigItem.name.ilike(f"%{keyword}%"))
|
|
|
|
q = select(ConfigItem)
|
|
if conditions:
|
|
q = q.where(and_(*conditions))
|
|
q = q.order_by(desc(ConfigItem.created_at)).limit(limit).offset(offset)
|
|
rows = (await db.execute(q)).scalars().all()
|
|
return rows
|
|
|
|
|
|
@router.get("/ci/stats")
|
|
async def ci_stats(
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
"""CI 현황 통계."""
|
|
total = (await db.execute(
|
|
select(func.count()).select_from(ConfigItem)
|
|
)).scalar() or 0
|
|
|
|
by_type = {}
|
|
type_rows = (await db.execute(
|
|
select(ConfigItem.ci_type, func.count())
|
|
.group_by(ConfigItem.ci_type)
|
|
)).all()
|
|
for ci_type, cnt in type_rows:
|
|
by_type[ci_type] = cnt
|
|
|
|
by_status = {}
|
|
status_rows = (await db.execute(
|
|
select(ConfigItem.status, func.count())
|
|
.group_by(ConfigItem.status)
|
|
)).all()
|
|
for st, cnt in status_rows:
|
|
by_status[st] = cnt
|
|
|
|
relation_count = (await db.execute(
|
|
select(func.count()).select_from(CIRelation)
|
|
)).scalar() or 0
|
|
|
|
change_count = (await db.execute(
|
|
select(func.count()).select_from(CIChangeLog)
|
|
)).scalar() or 0
|
|
|
|
return {
|
|
"total_ci": total,
|
|
"by_type": by_type,
|
|
"by_status": by_status,
|
|
"total_relations": relation_count,
|
|
"total_changes": change_count,
|
|
}
|
|
|
|
|
|
@router.get("/ci/{ci_id}", response_model=ConfigItemOut)
|
|
async def get_ci(
|
|
ci_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
"""CI 상세 조회."""
|
|
ci = (await db.execute(
|
|
select(ConfigItem).where(ConfigItem.ci_id == ci_id)
|
|
)).scalars().first()
|
|
if not ci:
|
|
raise HTTPException(404, f"CI {ci_id}를 찾을 수 없습니다.")
|
|
return ci
|
|
|
|
|
|
@router.patch("/ci/{ci_id}", response_model=ConfigItemOut)
|
|
async def update_ci(
|
|
ci_id: str,
|
|
body: ConfigItemUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""CI 수정 — 변경된 필드마다 이력 기록."""
|
|
ci = (await db.execute(
|
|
select(ConfigItem).where(ConfigItem.ci_id == ci_id)
|
|
)).scalars().first()
|
|
if not ci:
|
|
raise HTTPException(404, f"CI {ci_id}를 찾을 수 없습니다.")
|
|
|
|
update_data = body.model_dump(exclude_unset=True, exclude_none=True)
|
|
sr_id_for_log = update_data.pop("sr_id", None)
|
|
|
|
for field, new_val in update_data.items():
|
|
if field == "attributes":
|
|
old_v = ci.attributes_json
|
|
ci.attributes_json = json.dumps(new_val, ensure_ascii=False) if new_val else None
|
|
new_v = ci.attributes_json
|
|
else:
|
|
old_v = str(getattr(ci, field, "")) if getattr(ci, field, None) is not None else None
|
|
setattr(ci, field, new_val)
|
|
new_v = str(new_val)
|
|
|
|
# 상태 변경은 별도 change_type 사용
|
|
ct = CIChangeType.STATUS_CHANGE.value if field == "status" else CIChangeType.UPDATE.value
|
|
await _log_ci_change(
|
|
db, ci, ct,
|
|
changed_by = current_user.username,
|
|
field_name = field,
|
|
old_value = old_v,
|
|
new_value = new_v,
|
|
sr_id = sr_id_for_log,
|
|
)
|
|
|
|
await db.commit()
|
|
await db.refresh(ci)
|
|
return ci
|
|
|
|
|
|
@router.delete("/ci/{ci_id}", status_code=204)
|
|
async def retire_ci(
|
|
ci_id: str,
|
|
note: Optional[str] = Query(None, description="폐기 사유"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""CI 폐기 처리 (RETIRED 상태 전환 — 데이터 보존)."""
|
|
ci = (await db.execute(
|
|
select(ConfigItem).where(ConfigItem.ci_id == ci_id)
|
|
)).scalars().first()
|
|
if not ci:
|
|
raise HTTPException(404, f"CI {ci_id}를 찾을 수 없습니다.")
|
|
if ci.status == "RETIRED":
|
|
raise HTTPException(400, "이미 폐기된 CI입니다.")
|
|
|
|
old_status = ci.status
|
|
ci.status = "RETIRED"
|
|
ci.retire_date = datetime.utcnow().date()
|
|
await _log_ci_change(
|
|
db, ci,
|
|
change_type = CIChangeType.RETIRE.value,
|
|
changed_by = current_user.username,
|
|
field_name = "status",
|
|
old_value = old_status,
|
|
new_value = "RETIRED",
|
|
note = note or "폐기 처리",
|
|
)
|
|
await db.commit()
|
|
|
|
|
|
# ── CI 관계 관리 ─────────────────────────────────────────────────────────────
|
|
|
|
@router.post("/ci/relations", response_model=CIRelationOut, status_code=201)
|
|
async def add_ci_relation(
|
|
body: CIRelationCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""CI 간 관계 추가."""
|
|
from_ci = (await db.execute(select(ConfigItem).where(ConfigItem.id == body.from_ci_id))).scalars().first()
|
|
to_ci = (await db.execute(select(ConfigItem).where(ConfigItem.id == body.to_ci_id))).scalars().first()
|
|
if not from_ci:
|
|
raise HTTPException(404, f"출발 CI id={body.from_ci_id} 없음")
|
|
if not to_ci:
|
|
raise HTTPException(404, f"도착 CI id={body.to_ci_id} 없음")
|
|
|
|
# 중복 검사
|
|
existing = (await db.execute(
|
|
select(CIRelation).where(
|
|
and_(
|
|
CIRelation.from_ci_id == body.from_ci_id,
|
|
CIRelation.to_ci_id == body.to_ci_id,
|
|
CIRelation.relation_type == body.relation_type.upper(),
|
|
)
|
|
)
|
|
)).scalars().first()
|
|
if existing:
|
|
raise HTTPException(409, "동일 관계가 이미 존재합니다.")
|
|
|
|
relation = CIRelation(
|
|
from_ci_id = body.from_ci_id,
|
|
to_ci_id = body.to_ci_id,
|
|
relation_type = body.relation_type.upper(),
|
|
description = body.description,
|
|
created_by = current_user.username,
|
|
)
|
|
db.add(relation)
|
|
await db.flush()
|
|
|
|
# 양쪽 CI에 이력 기록
|
|
for ci, note in [
|
|
(from_ci, f"{body.relation_type} → CI#{body.to_ci_id}({to_ci.name}) 관계 추가"),
|
|
(to_ci, f"CI#{body.from_ci_id}({from_ci.name}) ← {body.relation_type} 관계 추가"),
|
|
]:
|
|
await _log_ci_change(
|
|
db, ci,
|
|
change_type = CIChangeType.RELATION_ADD.value,
|
|
changed_by = current_user.username,
|
|
note = note,
|
|
)
|
|
|
|
await db.commit()
|
|
await db.refresh(relation)
|
|
return relation
|
|
|
|
|
|
@router.delete("/ci/relations/{relation_id}", status_code=204)
|
|
async def remove_ci_relation(
|
|
relation_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""CI 관계 삭제."""
|
|
rel = (await db.execute(select(CIRelation).where(CIRelation.id == relation_id))).scalars().first()
|
|
if not rel:
|
|
raise HTTPException(404, "관계를 찾을 수 없습니다.")
|
|
|
|
from_ci = (await db.execute(select(ConfigItem).where(ConfigItem.id == rel.from_ci_id))).scalars().first()
|
|
to_ci = (await db.execute(select(ConfigItem).where(ConfigItem.id == rel.to_ci_id))).scalars().first()
|
|
|
|
for ci in [c for c in [from_ci, to_ci] if c]:
|
|
await _log_ci_change(
|
|
db, ci,
|
|
change_type = CIChangeType.RELATION_DEL.value,
|
|
changed_by = current_user.username,
|
|
note = f"관계(id={relation_id}, type={rel.relation_type}) 삭제",
|
|
)
|
|
|
|
await db.delete(rel)
|
|
await db.commit()
|
|
|
|
|
|
@router.get("/ci/{ci_id}/relations")
|
|
async def get_ci_relations(
|
|
ci_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
"""CI의 모든 관계 조회 (출발+도착 모두)."""
|
|
ci = (await db.execute(select(ConfigItem).where(ConfigItem.ci_id == ci_id))).scalars().first()
|
|
if not ci:
|
|
raise HTTPException(404, f"CI {ci_id} 없음")
|
|
|
|
# 출발 관계
|
|
out_rels = (await db.execute(
|
|
select(CIRelation).where(CIRelation.from_ci_id == ci.id)
|
|
)).scalars().all()
|
|
|
|
# 도착 관계
|
|
in_rels = (await db.execute(
|
|
select(CIRelation).where(CIRelation.to_ci_id == ci.id)
|
|
)).scalars().all()
|
|
|
|
# 관련 CI 이름 보강
|
|
async def enrich(rel: CIRelation, direction: str):
|
|
other_id = rel.to_ci_id if direction == "outbound" else rel.from_ci_id
|
|
other_ci = (await db.execute(select(ConfigItem).where(ConfigItem.id == other_id))).scalars().first()
|
|
return {
|
|
"id": rel.id,
|
|
"direction": direction,
|
|
"relation_type": rel.relation_type,
|
|
"from_ci_id": rel.from_ci_id,
|
|
"to_ci_id": rel.to_ci_id,
|
|
"other_ci_id": other_ci.ci_id if other_ci else None,
|
|
"other_ci_name": other_ci.name if other_ci else None,
|
|
"description": rel.description,
|
|
"created_at": rel.created_at.isoformat(),
|
|
}
|
|
|
|
result = []
|
|
for r in out_rels:
|
|
result.append(await enrich(r, "outbound"))
|
|
for r in in_rels:
|
|
result.append(await enrich(r, "inbound"))
|
|
|
|
return {"ci_id": ci_id, "relations": result}
|
|
|
|
|
|
# ── CI 변경 이력 ─────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/ci/{ci_id}/history", response_model=List[CIChangeLogOut])
|
|
async def get_ci_history(
|
|
ci_id: str,
|
|
limit: int = Query(50, ge=1, le=200),
|
|
db: AsyncSession = Depends(get_db),
|
|
_u: User = Depends(get_current_user),
|
|
):
|
|
"""CI 변경 이력 조회."""
|
|
ci = (await db.execute(select(ConfigItem).where(ConfigItem.ci_id == ci_id))).scalars().first()
|
|
if not ci:
|
|
raise HTTPException(404, f"CI {ci_id} 없음")
|
|
|
|
logs = (await db.execute(
|
|
select(CIChangeLog)
|
|
.where(CIChangeLog.ci_id_fk == ci.id)
|
|
.order_by(desc(CIChangeLog.changed_at))
|
|
.limit(limit)
|
|
)).scalars().all()
|
|
return logs
|
|
|
|
|
|
# ── 서버 → CI 일괄 등록 ───────────────────────────────────────────────────────
|
|
|
|
@router.post("/ci/import-servers")
|
|
async def import_servers_as_ci(
|
|
inst_id: Optional[int] = Query(None, description="특정 기관만 가져오기"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
기존 등록된 서버를 CI로 일괄 등록 (이미 CI가 있는 서버는 스킵).
|
|
"""
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.PM, UserRole.ENGINEER):
|
|
raise HTTPException(403, "권한 부족")
|
|
|
|
q = select(Server).where(Server.is_active == True)
|
|
if inst_id:
|
|
q = q.where(Server.inst_id == inst_id)
|
|
servers = (await db.execute(q)).scalars().all()
|
|
|
|
created, skipped = 0, 0
|
|
for srv in servers:
|
|
# 이미 linked_server_id로 CI가 있으면 스킵
|
|
existing = (await db.execute(
|
|
select(ConfigItem).where(ConfigItem.linked_server_id == srv.id)
|
|
)).scalars().first()
|
|
if existing:
|
|
skipped += 1
|
|
continue
|
|
|
|
ci_id = await _next_ci_id(db)
|
|
# server_role → ci_type 매핑
|
|
type_map = {
|
|
"WEB": "SERVER", "WAS": "SERVER", "DB": "DATABASE",
|
|
"ESB": "MIDDLEWARE", "BATCH": "SERVER",
|
|
}
|
|
srv_role = getattr(srv, "server_role", "") or ""
|
|
ci_type = type_map.get(srv_role.upper(), "SERVER")
|
|
|
|
ci = ConfigItem(
|
|
ci_id = ci_id,
|
|
name = srv.server_name,
|
|
ci_type = ci_type,
|
|
category = srv_role or "SERVER",
|
|
status = "ACTIVE" if srv.is_active else "INACTIVE",
|
|
owner = srv.maintenance_contact,
|
|
install_date = srv.install_date,
|
|
retire_date = srv.eol_date,
|
|
linked_server_id = srv.id,
|
|
inst_id = srv.inst_id,
|
|
description = f"서버 자동 등록: {srv.server_name} ({srv.os_type or ''})",
|
|
created_by = current_user.username,
|
|
)
|
|
db.add(ci)
|
|
await db.flush()
|
|
await _log_ci_change(
|
|
db, ci,
|
|
change_type = CIChangeType.CREATE.value,
|
|
changed_by = current_user.username,
|
|
note = f"서버 일괄 등록 (server_id={srv.id})",
|
|
)
|
|
created += 1
|
|
|
|
await db.commit()
|
|
return {
|
|
"created": created,
|
|
"skipped": skipped,
|
|
"total_servers": len(servers),
|
|
}
|