691 lines
30 KiB
HTML
691 lines
30 KiB
HTML
{% 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 %} |