update
This commit is contained in:
@@ -0,0 +1,691 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}스크립트 관리{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-code-square"></i> 스크립트 관리</h2>
|
||||
<button class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#yamlGuideModal">
|
||||
<i class="bi bi-question-circle"></i> YAML 작성 가이드
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<ul class="nav nav-tabs mb-3" id="scriptTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="mac-tab" data-bs-toggle="tab" data-bs-target="#mac" type="button">
|
||||
<i class="bi bi-ethernet"></i> MAC 수집
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="server-tab" data-bs-toggle="tab" data-bs-target="#server" type="button">
|
||||
<i class="bi bi-server"></i> 서버 정보
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="gpu-tab" data-bs-toggle="tab" data-bs-target="#gpu" type="button">
|
||||
<i class="bi bi-gpu-card"></i> GPU 서버 정보
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="terminal-tab" data-bs-toggle="tab" data-bs-target="#terminal" type="button">
|
||||
<i class="bi bi-terminal"></i> 터미널
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 탭 컨텐츠 -->
|
||||
<div class="tab-content" id="scriptTabContent">
|
||||
<!-- MAC 수집 탭 -->
|
||||
<div class="tab-pane fade show active" id="mac" role="tabpanel">
|
||||
<div class="row g-3" id="macProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 서버 정보 탭 -->
|
||||
<div class="tab-pane fade" id="server" role="tabpanel">
|
||||
<div class="row g-3" id="serverProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU 서버 정보 탭 -->
|
||||
<div class="tab-pane fade" id="gpu" role="tabpanel">
|
||||
<div class="row g-3" id="gpuProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 터미널 탭 (Racadm Tester) -->
|
||||
<div class="tab-pane fade" id="terminal" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h5 class="mb-0"><i class="bi bi-terminal-fill me-2"></i>Racadm Command Tester</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form id="racadmForm" onsubmit="event.preventDefault(); runRacadmCommand();">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">iDRAC IP</label>
|
||||
<input type="text" class="form-control" id="targetIp" placeholder="192.168.0.1"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Username</label>
|
||||
<input type="text" class="form-control" id="targetUser" value="root">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Password</label>
|
||||
<input type="password" class="form-control" id="targetPass" value="calvin">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-dark w-100" id="btnRunCommand">
|
||||
<i class="bi bi-play-fill me-1"></i> Run Command
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold">Command</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text font-monospace bg-light">racadm</span>
|
||||
<input type="text" class="form-control font-monospace" id="targetCmd"
|
||||
placeholder="getsysinfo" value="getsysinfo" required>
|
||||
</div>
|
||||
<div class="form-text">Note: 'racadm' is automatically prepended. Just enter the
|
||||
arguments (e.g. 'getsysinfo' or 'get NIC.NICConfig.1').</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold mb-0">Output</label>
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
onclick="document.getElementById('cmdOutput').innerText=''">
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-dark text-light p-3 rounded font-monospace"
|
||||
style="min-height: 200px; overflow: auto; resize: vertical;">
|
||||
<pre id="cmdOutput" class="mb-0 text-break" style="white-space: pre-wrap;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- YAML 가이드 모달 -->
|
||||
<div class="modal fade" id="yamlGuideModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-book"></i> YAML 프로파일 작성 가이드</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 탭 메뉴 -->
|
||||
<ul class="nav nav-tabs mb-3" id="yamlGuideTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#guide-basic"><i
|
||||
class="bi bi-lightbulb"></i> YAML 기초</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-mac">MAC 수집</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-server">서버 정보</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-gpu">GPU/GUID</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 탭 컨텐츠 -->
|
||||
<div class="tab-content border p-3 rounded bg-light" style="max-height: 60vh; overflow-y: auto;">
|
||||
|
||||
<!-- 1. YAML 기초 -->
|
||||
<div class="tab-pane fade show active" id="guide-basic">
|
||||
<h5 class="mb-3 text-primary">YAML 파일 작성 규칙 (초보자 필독)</h5>
|
||||
<div class="alert alert-warning">
|
||||
<strong><i class="bi bi-exclamation-triangle-fill"></i> 가장 중요한 규칙 2가지</strong>
|
||||
<ol class="mb-0 mt-2">
|
||||
<li><strong>들여쓰기(띄어쓰기)</strong>: 절대 <code>Tab</code> 키를 쓰지 마세요! <span
|
||||
class="badge bg-danger">스페이스바 2칸</span>을 사용해야 합니다.</li>
|
||||
<li><strong>콜론(:) 다음 공백</strong>: <code>key:value</code> (X) -> <code>key: value</code>
|
||||
(O) <br>콜론 뒤에는 반드시 한 칸을 띄워야 합니다.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header fw-bold">구조 이해하기</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">설정은 크게 <strong>이름(Key)</strong>과 <strong>값(Value)</strong>으로 나뉩니다.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-list-ul"></i> 목록 (List) 작성법</h6>
|
||||
<p class="small text-muted">여러 개의 값을 넣으려면 대쉬(<code>-</code>)를 사용합니다.</p>
|
||||
<pre class="bg-dark text-light p-2 rounded small"><code># 과일 목록 예시
|
||||
fruits:
|
||||
- "사과" # 리스트 1
|
||||
- "바나나" # 리스트 2
|
||||
- "포도" # 리스트 3</code></pre>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-card-text"></i> 설정 (Object) 작성법</h6>
|
||||
<p class="small text-muted">하위 설정을 넣으려면 들여쓰기를 합니다.</p>
|
||||
<pre class="bg-dark text-light p-2 rounded small"><code># 자동차 설정 예시
|
||||
car:
|
||||
color: "Red" # 스페이스 2칸
|
||||
speed: 100 # 스페이스 2칸
|
||||
is_active: true # 스페이스 2칸</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. MAC 수집 -->
|
||||
<div class="tab-pane fade" id="guide-mac">
|
||||
<h5 class="mb-3">MAC 주소 수집 프로파일</h5>
|
||||
<p class="text-muted">서버의 네트워크 카드(NIC)의 물리적 주소(MAC)를 가져오는 설정입니다.</p>
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<div class="card-header bg-primary text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "기본 MAC 수집 설정"
|
||||
|
||||
# 1. 수집할 NIC(네트워크 카드)의 이름 패턴
|
||||
nic_patterns:
|
||||
- "NIC.Integrated.1" # 메인보드 내장 랜카드 1번
|
||||
- "NIC.Slot.*" # 슬롯에 꽂힌 모든 랜카드 (PCIe)
|
||||
|
||||
# 2. InfiniBand(고속 네트워크) 카드가 있다면 슬롯 번호를 적으세요
|
||||
infiniband_slots:
|
||||
- "PCIeSlot3" # 3번 슬롯에 꽂힌 카드
|
||||
|
||||
# 3. iDRAC 전용 포트의 MAC도 필요하면 true로 설정
|
||||
include_idrac_mac: true</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4"><i class="bi bi-question-circle-fill"></i> 상세 설명</h6>
|
||||
<table class="table table-sm table-bordered bg-white">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>항목</th>
|
||||
<th>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>nic_patterns</code></td>
|
||||
<td>가져올 랜카드의 이름입니다. <code>*</code>(별표)는 '모두'를 의미합니다.<br>예: <code>NIC.*</code>는 모든
|
||||
랜카드를 다 가져옵니다.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>infiniband_slots</code></td>
|
||||
<td>InfiniBand는 일반 NIC와 달라서 슬롯 번호(예: PCIeSlot3)를 정확히 적어야 합니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 3. 서버 정보 -->
|
||||
<div class="tab-pane fade" id="guide-server">
|
||||
<h5 class="mb-3">서버 정보 수집 프로파일</h5>
|
||||
<p class="text-muted">서버의 펌웨어, BIOS 설정, 하드웨어 상태 등을 확인하는 설정입니다.</p>
|
||||
|
||||
<div class="card mb-3 border-success">
|
||||
<div class="card-header bg-success text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "서버 점검용 설정"
|
||||
|
||||
# 1. 버전 정보를 알고 싶은 펌웨어 이름들
|
||||
firmware:
|
||||
- "iDRAC"
|
||||
- "BIOS"
|
||||
- "NIC"
|
||||
|
||||
# 2. 확인하고 싶은 BIOS 설정값
|
||||
bios_settings:
|
||||
- "MemTest" # 메모리 테스트 옵션
|
||||
- "LogicalProc" # 하이퍼스레딩(논리코어)
|
||||
|
||||
# 3. RAID 컨트롤러의 디스크 정보
|
||||
raid_settings:
|
||||
- "Disk.Bay.0" # 0번 베이의 하드디스크 정보
|
||||
|
||||
# 4. 기타 명령어 실행 (상급자용)
|
||||
custom_items:
|
||||
- command: "getsysinfo"
|
||||
label: "전체 시스템 요약"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. GPU/GUID -->
|
||||
<div class="tab-pane fade" id="guide-gpu">
|
||||
<h5 class="mb-3">GPU 및 GUID 설정</h5>
|
||||
<p class="text-muted">GPU의 시리얼 번호를 수집하거나, iDRAC MAC 주소를 기반으로 GUID를 생성할 때 사용합니다.</p>
|
||||
|
||||
<div class="card mb-3 border-danger">
|
||||
<div class="card-header bg-danger text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "GPU 서버용 설정"
|
||||
|
||||
# 1. GPU 시리얼 수집 설정 (GPU가 있는 경우 필수)
|
||||
gpu_settings:
|
||||
target_slots:
|
||||
- "Video.Slot.*" # 모든 비디오 카드 수집
|
||||
sort_order: "slot_index" # 슬롯 순서대로 정렬
|
||||
|
||||
# 2. GUID 생성 시 슬롯 우선순위
|
||||
# iDRAC MAC 말고, 특정 랜카드의 MAC을 GUID로 쓰고 싶을 때 사용
|
||||
slot_priority:
|
||||
- 38 # 38번 슬롯 카드의 MAC을 1순위로 사용
|
||||
- 1 # 없으면 1번 슬롯 사용
|
||||
|
||||
output_format: "combined"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-light border">
|
||||
<strong><i class="bi bi-lightbulb"></i> 팁:</strong> GPU 시리얼만 필요하다면
|
||||
<code>slot_priority</code> 부분은 지워도 됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 프로파일 편집 모달 -->
|
||||
<div class="modal fade" id="profileModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="profileModalTitle">프로파일 편집</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">프로파일 이름</label>
|
||||
<input type="text" class="form-control" id="profileName" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" id="profileDescription">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설정 (YAML)</label>
|
||||
<textarea class="form-control font-monospace" id="profileConfig" rows="15"></textarea>
|
||||
<small class="text-muted">YAML 형식으로 프로파일 설정을 편집할 수 있습니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveProfile()">
|
||||
<i class="bi bi-save"></i> 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.profile-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.profile-card .card-body {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
/* 탭 가시성 강제 설정 */
|
||||
.nav-tabs .nav-link {
|
||||
color: #495057 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #fff !important;
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #0a58ca !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentCategory = 'mac';
|
||||
let currentProfile = null;
|
||||
let allProfiles = [];
|
||||
|
||||
// 페이지 로드 시 프로파일 목록 가져오기
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadProfiles();
|
||||
});
|
||||
|
||||
// 프로파일 목록 로드
|
||||
async function loadProfiles() {
|
||||
// 로딩 표시
|
||||
['macProfiles', 'serverProfiles', 'gpuProfiles'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = '<div class="col-12 text-center py-5"><div class="spinner-border text-primary" role="status"></div><p class="mt-2 text-muted">프로파일을 불러오는 중입니다...</p></div>';
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/profiles');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
allProfiles = await response.json();
|
||||
|
||||
if (!Array.isArray(allProfiles)) throw new Error('데이터 형식이 올바르지 않습니다.');
|
||||
|
||||
renderProfiles();
|
||||
} catch (error) {
|
||||
console.error('프로파일 로드 오류:', error);
|
||||
// 에러 표시
|
||||
['macProfiles', 'serverProfiles', 'gpuProfiles'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = `<div class="col-12 text-center py-5"><i class="bi bi-exclamation-triangle text-danger fs-1"></i><p class="mt-2 text-danger">프로파일을 불러오지 못했습니다.<br><small>${error.message}</small></p></div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 렌더링
|
||||
function renderProfiles() {
|
||||
const gpuContainer = document.getElementById('gpuProfiles');
|
||||
const categories = {
|
||||
'mac': document.getElementById('macProfiles'),
|
||||
'server_info': document.getElementById('serverProfiles'),
|
||||
'guid': gpuContainer,
|
||||
'gpu': gpuContainer
|
||||
};
|
||||
|
||||
// 각 카테고리 초기화
|
||||
Object.values(categories).forEach(el => { if (el) el.innerHTML = ''; });
|
||||
|
||||
// 프로파일 카드 생성
|
||||
allProfiles.forEach(profile => {
|
||||
const container = categories[profile.category];
|
||||
if (!container) return;
|
||||
|
||||
const card = createProfileCard(profile);
|
||||
container.insertAdjacentHTML('beforeend', card);
|
||||
});
|
||||
|
||||
// 새 프로파일 추가 카드
|
||||
const targets = [
|
||||
{ cat: 'mac', el: document.getElementById('macProfiles') },
|
||||
{ cat: 'server_info', el: document.getElementById('serverProfiles') },
|
||||
{ cat: 'gpu', el: gpuContainer }
|
||||
];
|
||||
|
||||
targets.forEach(t => {
|
||||
if (t.el) t.el.insertAdjacentHTML('beforeend', createNewProfileCard(t.cat));
|
||||
});
|
||||
}
|
||||
|
||||
// 프로파일 카드 HTML 생성
|
||||
function createProfileCard(profile) {
|
||||
const badgeClass = profile.is_default ? 'bg-success' : 'bg-secondary';
|
||||
const badgeText = profile.is_default ? '기본' : '사용자 정의';
|
||||
|
||||
return `
|
||||
<div class="col-md-4">
|
||||
<div class="card profile-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<h5 class="card-title">
|
||||
${profile.is_default ? '<i class="bi bi-star-fill text-warning"></i>' : ''}
|
||||
${profile.name}
|
||||
</h5>
|
||||
<p class="card-text text-muted small">${profile.description}</p>
|
||||
</div>
|
||||
<span class="badge ${badgeClass}">${badgeText}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">수집 항목:</small>
|
||||
<div class="mt-1">
|
||||
<span class="badge bg-light text-dark">${profile.items}개</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary flex-fill" onclick="editProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-pencil"></i> 편집
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="exportProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-download"></i> 내보내기
|
||||
</button>
|
||||
${!profile.is_default ? `
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 새 프로파일 추가 카드
|
||||
function createNewProfileCard(category) {
|
||||
return `
|
||||
<div class="col-md-4">
|
||||
<div class="card profile-card border-dashed" style="border: 2px dashed #dee2e6;">
|
||||
<div class="card-body d-flex flex-column align-items-center justify-content-center" style="min-height: 180px;">
|
||||
<i class="bi bi-plus-circle fs-1 text-muted mb-3"></i>
|
||||
<button class="btn btn-outline-primary" onclick="createNewProfile('${category}')">
|
||||
새 프로파일 만들기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 프로파일 편집
|
||||
async function editProfile(category, name) {
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${category}/${name}`);
|
||||
const profile = await response.json();
|
||||
|
||||
currentCategory = category;
|
||||
currentProfile = name;
|
||||
|
||||
document.getElementById('profileName').value = name;
|
||||
document.getElementById('profileDescription').value = profile.description;
|
||||
document.getElementById('profileConfig').value = JSON.stringify(profile.config, null, 2);
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('profileModal'));
|
||||
modal.show();
|
||||
} catch (error) {
|
||||
console.error('프로파일 로드 오류:', error);
|
||||
alert('프로파일을 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 저장
|
||||
async function saveProfile() {
|
||||
try {
|
||||
const config = JSON.parse(document.getElementById('profileConfig').value);
|
||||
|
||||
const response = await fetch(`/api/profiles/${currentCategory}/${currentProfile}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config: config
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('프로파일이 저장되었습니다.');
|
||||
bootstrap.Modal.getInstance(document.getElementById('profileModal')).hide();
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('저장 실패: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 오류:', error);
|
||||
alert('프로파일 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 내보내기
|
||||
function exportProfile(category, name) {
|
||||
window.location.href = `/api/profiles/${category}/${name}/export`;
|
||||
}
|
||||
|
||||
// 프로파일 삭제
|
||||
async function deleteProfile(category, name) {
|
||||
if (!confirm(`'${name}' 프로파일을 삭제하시겠습니까?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${category}/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('프로파일이 삭제되었습니다.');
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('삭제 실패: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 오류:', error);
|
||||
alert('프로파일 삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 새 프로파일 만들기
|
||||
function createNewProfile(category) {
|
||||
const name = prompt('새 프로파일 이름을 입력하세요:');
|
||||
if (!name) return;
|
||||
|
||||
// 기본 설정으로 프로파일 생성
|
||||
const defaultConfig = {
|
||||
description: "새 프로파일",
|
||||
extends: "default"
|
||||
};
|
||||
|
||||
fetch(`/api/profiles/${category}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
config: defaultConfig
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('프로파일이 생성되었습니다.');
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('생성 실패: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('생성 오류:', error);
|
||||
alert('프로파일 생성 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
async function runRacadmCommand() {
|
||||
const ip = document.getElementById('targetIp').value;
|
||||
const user = document.getElementById('targetUser').value;
|
||||
const pass = document.getElementById('targetPass').value;
|
||||
const cmd = document.getElementById('targetCmd').value;
|
||||
const outputPre = document.getElementById('cmdOutput');
|
||||
const btn = document.getElementById('btnRunCommand');
|
||||
|
||||
if (!ip || !user || !pass || !cmd) {
|
||||
alert('모든 필드를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// UI Reset
|
||||
outputPre.innerText = "Executing...";
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Running...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test_racadm', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ip: ip,
|
||||
user: user,
|
||||
password: pass,
|
||||
command: cmd
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Return code가 0이 아니면 실패로 간주하거나, 그대로 출력
|
||||
let displayText = `[Success]\n${result.output}`;
|
||||
if (result.return_code !== 0) {
|
||||
displayText = `[Error (Code: ${result.return_code})]\n${result.output}`;
|
||||
}
|
||||
outputPre.innerText = displayText;
|
||||
} else {
|
||||
outputPre.innerText = `[Error]\n${result.error}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
outputPre.innerText = `[System Error]\n${error.message}`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-1"></i> Run Command';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user