update
This commit is contained in:
@@ -12,8 +12,7 @@
|
||||
/* 초기 높이 */
|
||||
min-height: 300px;
|
||||
/* 최소 높이 */
|
||||
max-height: 1200px;
|
||||
/* 최대 높이 (선택 사항) */
|
||||
/* max-height 제거하여 제한 없이 확장 가능하게 함 */
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
@@ -21,7 +20,7 @@
|
||||
overflow: hidden;
|
||||
/* resize를 위해 필수 */
|
||||
resize: vertical;
|
||||
/* 수직 리사이징 활성화 */
|
||||
/* 수직 리사이징만 활성화 (수평 고정) */
|
||||
position: relative;
|
||||
/* 자식 요소 relative 기준 */
|
||||
}
|
||||
|
||||
@@ -86,6 +86,16 @@
|
||||
<i class="bi bi-list-task me-1"></i>Job Monitor
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('bios_baseline.index') }}">
|
||||
<i class="bi bi-clipboard-data me-1"></i>BIOS Baseline
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('script_manager.index') }}">
|
||||
<i class="bi bi-code-square me-1"></i>스크립트 관리
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin.admin_panel') }}">
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* 모달이 Navbar 위에 오도록 z-index 조정 */
|
||||
.modal {
|
||||
z-index: 1060 !important;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 1050 !important;
|
||||
}
|
||||
|
||||
/* 모달 헤더가 항상 잘 보이도록 배경색 및 위치 조정 */
|
||||
.modal-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1070;
|
||||
}
|
||||
|
||||
/* 모달이 너무 위로 붙지 않게 여백 추가 */
|
||||
.modal-dialog {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* ===== 탭 가시성 개선 ===== */
|
||||
/* Pills 스타일 탭 버튼 */
|
||||
.nav-pills .nav-link {
|
||||
color: #495057 !important;
|
||||
background-color: #e9ecef !important;
|
||||
border: 1px solid #ced4da !important;
|
||||
margin-right: 0.25rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link:hover {
|
||||
background-color: #dee2e6 !important;
|
||||
border-color: #adb5bd !important;
|
||||
color: #212529 !important;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: #0d6efd !important;
|
||||
border-color: #0d6efd !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Select 드롭다운 가시성 개선 */
|
||||
.form-select {
|
||||
border: 2px solid #ced4da !important;
|
||||
font-weight: 500;
|
||||
color: #212529 !important;
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: #0d6efd !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
color: #212529 !important;
|
||||
background-color: #ffffff !important;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* 탭 컨테이너 배경 */
|
||||
.nav-pills-container {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2 class="fw-bold mb-1">
|
||||
<i class="bi bi-clipboard-data text-primary me-2"></i>
|
||||
BIOS Baseline 비교
|
||||
</h2>
|
||||
<p class="text-muted mb-0">납품 전 서버의 BIOS 설정을 표준 설정과 비교합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 작업 영역 -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- 왼쪽: IP 입력 및 설정 -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card border shadow-sm h-100">
|
||||
<div class="card-header bg-light border-0 py-2">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
비교 설정
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4 d-flex flex-column">
|
||||
|
||||
<!-- Baseline 선택 -->
|
||||
<div class="mb-3">
|
||||
<label for="baseline-select" class="form-label">Baseline 선택</label>
|
||||
<select id="baseline-select" class="form-select">
|
||||
<option value="">Baseline을 선택하세요</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary mt-2 w-100" onclick="showBaselineManageModal()">
|
||||
<i class="bi bi-plus-circle me-1"></i> 새 Baseline 생성
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IP 주소 입력 -->
|
||||
<div class="mb-3 flex-grow-1 d-flex flex-column">
|
||||
<label for="ip-addresses"
|
||||
class="form-label w-100 d-flex justify-content-between align-items-end mb-2">
|
||||
<span class="mb-1">
|
||||
서버 IP 주소
|
||||
<span class="badge bg-secondary ms-1" id="ip-count">0</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary px-2 py-1"
|
||||
onclick="clearIPs()" title="입력 내용 지우기" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-trash me-1"></i>지우기
|
||||
</button>
|
||||
</label>
|
||||
<textarea id="ip-addresses" class="form-control font-monospace flex-grow-1"
|
||||
placeholder="예: 10.10.0.1 10.10.0.2 10.10.0.3" style="resize: none;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 인증 정보 -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<input type="text" id="username" class="form-control" value="root" placeholder="Username">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="password" id="password" class="form-control" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비교 시작 버튼 -->
|
||||
<button onclick="startComparison()" class="btn btn-primary w-100 py-2">
|
||||
<i class="bi bi-arrows-angle-contract me-1"></i> 비교 시작
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: 결과 요약 -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card border shadow-sm h-100">
|
||||
<div class="card-header bg-light border-0 py-2">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-bar-chart me-2"></i>
|
||||
비교 결과 요약
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4" id="summary-section">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||
<p class="mb-0">비교를 시작하면 결과가 여기에 표시됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 진행률 -->
|
||||
<div class="row mb-4" id="progress-section" style="display: none;">
|
||||
<div class="col">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-activity text-primary me-2"></i>
|
||||
<span class="fw-semibold">처리 진행률</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 25px;">
|
||||
<div id="progress-bar"
|
||||
class="progress-bar progress-bar-striped progress-bar-animated bg-success"
|
||||
role="progressbar" style="width: 0%">
|
||||
<span class="fw-semibold">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 비교 결과 -->
|
||||
<div class="row" id="results-section" style="display: none;">
|
||||
<div class="col">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
상세 비교 결과
|
||||
</h6>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="toggleDiffOnly()" id="diff-only-btn">
|
||||
<i class="bi bi-funnel me-1"></i> 차이점만 보기
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="downloadResults()">
|
||||
<i class="bi bi-download me-1"></i> Excel 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4" id="results-container">
|
||||
<!-- 서버별 결과가 여기에 표시됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Baseline 관리 모달 -->
|
||||
<div class="modal fade" id="baseline-manage-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-clipboard-data me-2"></i>
|
||||
Baseline 관리
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- 탭 -->
|
||||
<div class="nav-pills-container">
|
||||
<ul class="nav nav-pills mb-0" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#create-tab">
|
||||
새로 생성
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#list-tab">
|
||||
목록 관리
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- 생성 탭 -->
|
||||
<div class="tab-pane fade show active" id="create-tab">
|
||||
<form id="create-baseline-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">서버 IP 주소 *</label>
|
||||
<input type="text" class="form-control" id="create-ip" placeholder="10.10.0.1" required>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="create-username" value="root">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Password *</label>
|
||||
<input type="password" class="form-control" id="create-password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Baseline 이름 *</label>
|
||||
<input type="text" class="form-control" id="create-name"
|
||||
placeholder="R6615_GPU_Standard_2024" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">서버 타입</label>
|
||||
<input type="text" class="form-control" id="create-type" placeholder="GPU Server">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<textarea class="form-control" id="create-description" rows="2"
|
||||
placeholder="표준 GPU 서버 설정"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-download me-1"></i> Redfish로 가져와서 생성
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 목록 탭 -->
|
||||
<div class="tab-pane fade" id="list-tab">
|
||||
<div id="baseline-list">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-hourglass-split"></i> 로딩 중...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Baseline 상세 편집 모달 -->
|
||||
<div class="modal fade" id="edit-baseline-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-pencil-square me-2"></i>
|
||||
Baseline 상세 편집
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body bg-light">
|
||||
<div class="row g-3">
|
||||
<!-- 왼쪽: 메타데이터 편집 -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-bold">기본 정보</div>
|
||||
<div class="card-body">
|
||||
<form id="edit-baseline-meta-form">
|
||||
<input type="hidden" id="edit-id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Baseline 이름</label>
|
||||
<input type="text" class="form-control" id="edit-name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">서버 모델</label>
|
||||
<input type="text" class="form-control bg-light" id="edit-model" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">서버 타입</label>
|
||||
<input type="text" class="form-control" id="edit-type">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">설명</label>
|
||||
<textarea class="form-control" id="edit-description" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<p class="mb-1"><i class="bi bi-calendar me-1"></i>생성일: <span
|
||||
id="edit-created-at">-</span></p>
|
||||
<p class="mb-1"><i class="bi bi-cpu me-1"></i>원본 IP: <span
|
||||
id="edit-source-ip">-</span></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: BIOS 설정 편집 -->
|
||||
<!-- 오른쪽: 상세 설정 편집 -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 border shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="nav-pills-container">
|
||||
<ul class="nav nav-pills mb-0" id="edit-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-edit-bios" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-bios" type="button">BIOS</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-idrac" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-idrac" type="button">iDRAC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-raid" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-raid" type="button">RAID</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-nic" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-nic" type="button">NIC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-boot" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-boot" type="button">Boot</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-firmware" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-firmware" type="button">Firmware</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content h-100">
|
||||
|
||||
<!-- BIOS 탭 -->
|
||||
<div class="tab-pane fade show active h-100" id="content-edit-bios">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-bios-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('bios')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-bios">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">설정 항목 (Key)</th>
|
||||
<th style="width: 40%;">설정값 (Value)</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- iDRAC 탭 -->
|
||||
<div class="tab-pane fade h-100" id="content-edit-idrac">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-idrac-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('idrac')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-idrac">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">설정 항목 (Key)</th>
|
||||
<th style="width: 40%;">설정값 (Value)</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAID 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-raid">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
RAID 구성은 복잡하므로 현재 JSON 직접 편집만 지원합니다. (Read-Only 권장)
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-raid"
|
||||
rows="15" style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- NIC 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-nic">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
NIC 설정은 JSON 형식으로 편집합니다.
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-nic" rows="15"
|
||||
style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Boot 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-boot">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
부팅 순서 및 설정을 JSON 형식으로 편집합니다.
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-boot"
|
||||
rows="15" style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Firmware 탭 -->
|
||||
<div class="tab-pane fade h-100" id="content-edit-firmware">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-firmware-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('firmware')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-firmware">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">컴포넌트</th>
|
||||
<th style="width: 40%;">버전</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="saveBaselineChanges()">
|
||||
<i class="bi bi-save me-1"></i> 변경사항 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/bios_baseline.js') }}?v={{ range(1, 100000) | random }}"></script>
|
||||
{% endblock %}
|
||||
@@ -5,152 +5,70 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Dell iDRAC 멀티 서버 펌웨어 관리</title>
|
||||
<title>Dell iDRAC 멀티 서버 관리</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/idrac_style.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🖥️ Dell iDRAC 멀티 서버 펌웨어 관리</h1>
|
||||
<p class="subtitle">납품 전 펌웨어 일괄 업데이트 시스템 - 버전 비교 기능</p>
|
||||
<h1>🖥️ Dell iDRAC 멀티 서버 관리</h1>
|
||||
<p class="subtitle">서버 등록, 연결 상태 확인 및 설정 정밀 분석 시스템</p>
|
||||
</header>
|
||||
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="tab-menu">
|
||||
<button class="tab-button active" onclick="showTab('servers')">서버 관리</button>
|
||||
<button class="tab-button" onclick="showTab('versions')">펌웨어 버전 관리</button>
|
||||
<button class="tab-button" onclick="showTab('comparison')">버전 비교 결과</button>
|
||||
</div>
|
||||
|
||||
<!-- 서버 관리 탭 -->
|
||||
<div id="servers-tab" class="tab-content active">
|
||||
<!-- 서버 관리 섹션 -->
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>📋 서버 관리</h2>
|
||||
<div class="header-actions">
|
||||
<button onclick="showAddServerModal()" class="btn btn-primary">+ 서버 추가</button>
|
||||
<button onclick="refreshServers()" class="btn btn-info">🔄 새로고침</button>
|
||||
</div>
|
||||
<!-- 서버 관리 섹션 -->
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>📋 서버 목록 관리</h2>
|
||||
<div class="header-actions">
|
||||
<button onclick="showAddServerModal()" class="btn btn-primary">+ 서버 추가</button>
|
||||
<a href="/server-config/" class="btn btn-secondary">⚙️ 설정 정밀 분석 및 DB 동기화</a>
|
||||
<button onclick="refreshServers()" class="btn btn-info">🔄 새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 필터 -->
|
||||
<div class="filter-group">
|
||||
<label>그룹:</label>
|
||||
<select id="group-filter" onchange="filterByGroup()">
|
||||
<option value="all">전체</option>
|
||||
</select>
|
||||
<span id="server-count" class="count-badge">0대</span>
|
||||
</div>
|
||||
<!-- 그룹 필터 -->
|
||||
<div class="filter-group">
|
||||
<label>그룹:</label>
|
||||
<select id="group-filter" onchange="filterByGroup()">
|
||||
<option value="all">전체</option>
|
||||
</select>
|
||||
<span id="server-count" class="count-badge">0대</span>
|
||||
</div>
|
||||
|
||||
<!-- 서버 목록 테이블 -->
|
||||
<div class="table-container">
|
||||
<table class="server-table" id="server-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40"><input type="checkbox" id="select-all" onchange="toggleSelectAll()"></th>
|
||||
<th>서버명</th>
|
||||
<th>IP 주소</th>
|
||||
<th>그룹</th>
|
||||
<th>상태</th>
|
||||
<th>BIOS 버전</th>
|
||||
<th>업데이트 상태</th>
|
||||
<th width="150">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="server-list">
|
||||
<tr>
|
||||
<td colspan="8" class="empty-message">등록된 서버가 없습니다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 서버 목록 테이블 -->
|
||||
<div class="table-container">
|
||||
<table class="server-table" id="server-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40"><input type="checkbox" id="select-all" onchange="toggleSelectAll()"></th>
|
||||
<th>서버명</th>
|
||||
<th>IP 주소</th>
|
||||
<th>그룹</th>
|
||||
<th>상태</th>
|
||||
<th>모델</th>
|
||||
<th>최근 연결</th>
|
||||
<th width="150">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="server-list">
|
||||
<tr>
|
||||
<td colspan="8" class="empty-message">등록된 서버가 없습니다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 선택 작업 -->
|
||||
<div class="bulk-actions">
|
||||
<span id="selected-count">선택: 0대</span>
|
||||
<button onclick="testSelectedConnections()" class="btn btn-info">연결 테스트</button>
|
||||
<button onclick="compareSelectedFirmware()" class="btn btn-warning">버전 비교</button>
|
||||
<button onclick="showUploadModal()" class="btn btn-primary">펌웨어 업로드</button>
|
||||
<button onclick="rebootSelected()" class="btn btn-danger">재부팅</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 업로드 진행 상황 -->
|
||||
<section class="card" id="upload-progress-section" style="display: none;">
|
||||
<h2>📊 업로드 진행 상황</h2>
|
||||
<div id="upload-progress-list"></div>
|
||||
<div class="progress-summary" id="progress-summary"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 펌웨어 버전 관리 탭 -->
|
||||
<div id="versions-tab" class="tab-content">
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>📦 펌웨어 최신 버전 관리</h2>
|
||||
<div class="header-actions">
|
||||
<button onclick="syncDellCatalog('PowerEdge R750')" class="btn btn-success">🔄 Dell에서 버전
|
||||
가져오기</button>
|
||||
<button onclick="syncFromDRM()" class="btn btn-info">📁 DRM에서 가져오기</button>
|
||||
<button onclick="showAddVersionModal()" class="btn btn-primary">+ 버전 추가</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="server-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>컴포넌트</th>
|
||||
<th>최신 버전</th>
|
||||
<th>서버 모델</th>
|
||||
<th>릴리즈 날짜</th>
|
||||
<th>중요</th>
|
||||
<th width="150">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="version-list">
|
||||
<tr>
|
||||
<td colspan="6" class="empty-message">등록된 버전 정보가 없습니다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 버전 비교 결과 탭 -->
|
||||
<div id="comparison-tab" class="tab-content">
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>🔍 펌웨어 버전 비교 결과</h2>
|
||||
<button onclick="compareSelectedFirmware()" class="btn btn-primary">🔄 다시 비교</button>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 상태 -->
|
||||
<div id="comparison-loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>버전 비교 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- 비교 결과 -->
|
||||
<div id="comparison-result">
|
||||
<div class="info-text">
|
||||
<p><strong>📋 사용 방법:</strong></p>
|
||||
<ol style="margin-top: 10px; margin-left: 20px; line-height: 1.8;">
|
||||
<li>"서버 관리" 탭에서 비교할 서버를 선택하세요</li>
|
||||
<li>"버전 비교" 버튼을 클릭하세요</li>
|
||||
<li>현재 설치된 펌웨어 버전과 최신 버전을 자동으로 비교합니다</li>
|
||||
<li>업데이트가 필요한 컴포넌트를 확인할 수 있습니다</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- 선택 작업 -->
|
||||
<div class="bulk-actions">
|
||||
<span id="selected-count">선택: 0대</span>
|
||||
<button onclick="testSelectedConnections()" class="btn btn-info">연결 테스트</button>
|
||||
<button onclick="rebootSelected()" class="btn btn-danger">재부팅</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>Dell iDRAC 멀티 서버 펌웨어 관리 시스템 - 버전 자동 비교</p>
|
||||
<p>Dell iDRAC 멀티 서버 관리 시스템</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -194,83 +112,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버전 추가 모달 -->
|
||||
<div id="add-version-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>펌웨어 최신 버전 추가</h3>
|
||||
<span class="close" onclick="closeAddVersionModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="version-component">컴포넌트명 *</label>
|
||||
<input type="text" id="version-component" placeholder="예: BIOS">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-latest">최신 버전 *</label>
|
||||
<input type="text" id="version-latest" placeholder="예: 2.15.0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-model">서버 모델 (선택)</label>
|
||||
<input type="text" id="version-model" placeholder="예: PowerEdge R750 (비우면 모든 모델)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-vendor">벤더</label>
|
||||
<input type="text" id="version-vendor" placeholder="예: Dell">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-release-date">릴리즈 날짜</label>
|
||||
<input type="date" id="version-release-date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-download-url">다운로드 URL</label>
|
||||
<input type="text" id="version-download-url" placeholder="DUP 파일 다운로드 링크">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version-notes">비고</label>
|
||||
<textarea id="version-notes" rows="3" placeholder="버전 변경 사항 등"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="version-critical">
|
||||
중요 업데이트
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeAddVersionModal()" class="btn btn-secondary">취소</button>
|
||||
<button onclick="addFirmwareVersion()" class="btn btn-primary">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 펌웨어 업로드 모달 -->
|
||||
<div id="upload-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>펌웨어 업로드</h3>
|
||||
<span class="close" onclick="closeUploadModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="warning-text">⚠️ 선택한 서버에 펌웨어를 일괄 업로드합니다</p>
|
||||
<div class="form-group">
|
||||
<label>선택한 서버: <strong id="upload-server-count">0대</strong></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="firmware-file">DUP 파일 선택 *</label>
|
||||
<input type="file" id="firmware-file" accept=".exe,.bin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeUploadModal()" class="btn btn-secondary">취소</button>
|
||||
<button onclick="startMultiUpload()" class="btn btn-primary">업로드 시작</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="card" id="result-section" style="display:none;">
|
||||
<div id="result-content"></div>
|
||||
</section>
|
||||
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/idrac_main.js') }}"></script>
|
||||
</body>
|
||||
|
||||
+500
-203
@@ -31,9 +31,37 @@
|
||||
<form id="ipForm" method="post" action="{{ url_for('main.process_ips') }}" class="h-100 d-flex flex-column">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
{# 스크립트 선택 #}
|
||||
{# 작업 유형 선택 #}
|
||||
<div class="mb-3">
|
||||
<select id="script" name="script" class="form-select" required autocomplete="off">
|
||||
<label for="job_type" class="form-label d-flex justify-content-between">
|
||||
<span>작업 유형</span>
|
||||
<a href="{{ url_for('script_manager.index') }}" class="text-decoration-none small">
|
||||
<i class="bi bi-gear-fill me-1"></i>프로파일 관리
|
||||
</a>
|
||||
</label>
|
||||
<select id="job_type" name="job_type" class="form-select" required>
|
||||
<option value="" disabled selected>작업을 선택하세요</option>
|
||||
<option value="mac">MAC 주소 수집</option>
|
||||
<option value="server_info">서버 정보 수집</option>
|
||||
<option value="guid">GUID 수집</option>
|
||||
<option value="gpu">GPU 시리얼 수집</option>
|
||||
<option value="legacy">기존 스크립트 사용 (Legacy)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# 프로파일 선택 (동적) #}
|
||||
<div class="mb-3" id="profileGroup" style="display:none;">
|
||||
<label for="profile" class="form-label">프로파일 선택</label>
|
||||
<select id="profile" name="profile" class="form-select">
|
||||
<option value="">프로파일을 선택하세요</option>
|
||||
</select>
|
||||
<div class="form-text small" id="profileDesc"></div>
|
||||
</div>
|
||||
|
||||
{# 기존 스크립트 선택 (Legacy) #}
|
||||
<div class="mb-3" id="legacyScriptGroup" style="display:none;">
|
||||
<label for="script" class="form-label">스크립트 선택</label>
|
||||
<select id="script" name="script" class="form-select" autocomplete="off">
|
||||
<option value="">스크립트를 선택하세요</option>
|
||||
{% if grouped_scripts %}
|
||||
{% for category, s_list in grouped_scripts.items() %}
|
||||
@@ -81,16 +109,16 @@
|
||||
</div>
|
||||
</label>
|
||||
<textarea id="ips" name="ips" class="form-control font-monospace flex-grow-1"
|
||||
placeholder="예: 192.168.1.1 192.168.1.2" required style="resize: none;"></textarea>
|
||||
placeholder="iDRAC IP 정보를 입력하세요 10.10.0.2 10.10.0.3" required style="resize: none;"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<button type="submit"
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move">
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
|
||||
<i class="bi bi-play-circle-fill fs-5"></i>
|
||||
<i class="bi bi-play-circle-fill fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.8rem;">처리 시작</span>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.75rem;">처리 시작</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -123,29 +151,29 @@
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}"
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
|
||||
<i class="bi bi-file-earmark-spreadsheet fs-5"></i>
|
||||
<i class="bi bi-file-earmark-spreadsheet fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.8rem;">MAC to Excel</span>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.75rem;">MAC to Excel</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button type="button" data-bs-toggle="modal" data-bs-target="#slotPriorityModal"
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 text-success p-1">
|
||||
<i class="bi bi-file-earmark-excel fs-5"></i>
|
||||
<i class="bi bi-file-earmark-excel fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.8rem;">GUID to Excel</span>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.75rem;">GUID to Excel</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button type="submit" formaction="{{ url_for('utils.update_gpu_list') }}"
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
class="btn btn-white bg-white border shadow-sm w-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move h-100">
|
||||
<div class="rounded-circle bg-danger bg-opacity-10 text-danger p-1">
|
||||
<i class="bi bi-gpu-card fs-5"></i>
|
||||
<i class="bi bi-gpu-card fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.8rem;">GPU to Excel</span>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.75rem;">GPU to Excel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,8 +183,104 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 진행바 #}
|
||||
{# 시스템 제어 패널 #}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-terminal me-2"></i>
|
||||
시스템 제어 (System Control)
|
||||
<small class="text-muted ms-2 fw-normal">- 위 "IP 주소" 입력란의 대상을 제어합니다.</small>
|
||||
</h6>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small id="systemControlHint" class="text-primary fw-bold"
|
||||
style="font-size: 0.75rem; cursor: pointer;">(클릭하여 열기)</small>
|
||||
<button class="btn btn-sm btn-link text-secondary p-0 text-decoration-none" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#systemControlPanel" aria-expanded="false"
|
||||
aria-controls="systemControlPanel">
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="systemControlPanel" class="collapse">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-2">
|
||||
<!-- Power ON -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('power_on')">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 text-success p-1">
|
||||
<i class="bi bi-power fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">Power ON</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Power OFF -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('power_off')">
|
||||
<div class="rounded-circle bg-danger bg-opacity-10 text-danger p-1">
|
||||
<i class="bi bi-person-x fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">Power OFF</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Log Clear -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('log_clear')">
|
||||
<div class="rounded-circle bg-warning bg-opacity-10 text-warning p-1">
|
||||
<i class="bi bi-eraser fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">Log Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Job Delete -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('job_delete')">
|
||||
<div class="rounded-circle bg-secondary bg-opacity-10 text-secondary p-1">
|
||||
<i class="bi bi-trash fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">Job Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- TSR Collect -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('tsr_collect')">
|
||||
<div class="rounded-circle bg-info bg-opacity-10 text-info p-1">
|
||||
<i class="bi bi-collection fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">TSR Collect</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- TSR Save -->
|
||||
<div class="col-md-2">
|
||||
<button
|
||||
class="btn btn-white bg-white border shadow-sm w-100 h-100 py-1 d-flex flex-column align-items-center justify-content-center gap-1 btn-quick-move"
|
||||
onclick="runControl('tsr_save')">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-1">
|
||||
<i class="bi bi-save fs-6"></i>
|
||||
</div>
|
||||
<span class="fw-medium text-dark" style="font-size: 0.7rem;">TSR Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 진행바 #}
|
||||
<div class="row mb-4" id="progressSection" style="display: none;">
|
||||
<div class="col">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
@@ -202,7 +326,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control border-primary-subtle form-control-sm"
|
||||
name="zip_filename" placeholder="파일명" required
|
||||
name="zip_filename" placeholder="파일명 또는 백업폴더명" required
|
||||
style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
|
||||
<button class="btn btn-primary btn-sm px-2" type="submit">
|
||||
<i class="bi bi-download" style="font-size: 0.75rem;"></i>
|
||||
@@ -295,213 +419,344 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 처리된 파일 목록 #}
|
||||
<div class="row mb-4 processed-list">
|
||||
{# 통합 파일 관리 섹션 (탭 인터페이스) #}
|
||||
<style>
|
||||
/* 탭/필 텍스트 가시성 강제 설정 (강제 덮어쓰기) */
|
||||
/* 기본(비활성) 상태: 무조건 검은색 */
|
||||
#fileTabs .nav-link,
|
||||
#repoTabs .nav-link {
|
||||
color: #000 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 메인 탭 활성 상태: 검은색 텍스트 + 흰색 배경 */
|
||||
#fileTabs .nav-link.active {
|
||||
color: #000 !important;
|
||||
background-color: #fff !important;
|
||||
border-color: #dee2e6 #dee2e6 #fff !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 서브 탭(Pills) 활성 상태: 흰색 텍스트 + 파란색 배경 */
|
||||
#repoTabs .nav-link.active {
|
||||
color: #fff !important;
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
/* 커스텀 탭 패널 가시성 제어 */
|
||||
.custom-tab-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.custom-tab-panel.custom-active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 커스텀 서브 탭 패널 가시성 제어 */
|
||||
.custom-sub-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.custom-sub-panel.custom-sub-active {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
<div class="row mb-5">
|
||||
<div class="col">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0 d-flex align-items-center">
|
||||
<i class="bi bi-files me-2"></i>
|
||||
처리된 파일 목록
|
||||
{% if files_to_display %}
|
||||
<span class="badge bg-primary ms-3">{{ files_to_display|length }} 파일</span>
|
||||
{% endif %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if files_to_display and files_to_display|length > 0 %}
|
||||
<div class="row g-3">
|
||||
{% for file_info in files_to_display %}
|
||||
<div class="col-auto">
|
||||
<div class="file-card-compact border rounded p-2 text-center">
|
||||
<a href="{{ url_for('main.download_file', filename=file_info.file) }}"
|
||||
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2" download
|
||||
title="{{ file_info.name or file_info.file }}">
|
||||
{{ file_info.name or file_info.file }}
|
||||
</a>
|
||||
<div class="file-card-buttons d-flex gap-2 justify-content-center">
|
||||
<button type="button" class="btn btn-sm btn-outline btn-view-processed flex-fill"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="idrac_info"
|
||||
data-filename="{{ file_info.file }}">
|
||||
보기
|
||||
</button>
|
||||
<form action="{{ url_for('main.delete_file', filename=file_info.file) }}" method="post"
|
||||
class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline btn-delete-processed flex-fill"
|
||||
onclick="return confirm('삭제하시겠습니까?');">
|
||||
삭제
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
{% if total_pages > 1 %}
|
||||
<nav aria-label="Processed files pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
|
||||
<!-- 이전 페이지 -->
|
||||
{% if page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.index', page=page-1) }}">
|
||||
<i class="bi bi-chevron-left"></i> 이전
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i> 이전</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- 페이지 번호 (최대 10개 표시) -->
|
||||
{% set start_page = ((page - 1) // 10) * 10 + 1 %}
|
||||
{% set end_page = [start_page + 9, total_pages]|min %}
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('main.index', page=p) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 다음 페이지 -->
|
||||
{% if page < total_pages %} <li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.index', page=page+1) }}">
|
||||
다음 <i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">다음 <i class="bi bi-chevron-right"></i></span>
|
||||
</li>
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<ul class="nav nav-tabs card-header-tabs" id="fileTabs" role="tablist">
|
||||
{# 탭 1: 처리된 파일 #}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="processed-tab" data-custom-toggle="tab" data-bs-target="#processed"
|
||||
type="button" role="tab" aria-controls="processed" aria-selected="true">
|
||||
<i class="bi bi-check-circle-fill text-success me-2"></i>처리된 파일
|
||||
{% if files_to_display %}
|
||||
<span class="badge bg-secondary ms-1 rounded-pill">{{ files_to_display|length }}</span>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<!-- /페이지네이션 -->
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">표시할 파일이 없습니다.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{# 탭 2: 백업 및 저장소 (드롭다운으로 할지, 서브탭으로 할지 결정. 여기서는 메인 탭 내부 서브탭 방식 채택) #}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="repository-tab" data-custom-toggle="tab" data-bs-target="#repository-panel"
|
||||
type="button" role="tab" aria-controls="repository-panel" aria-selected="false">
|
||||
<i class="bi bi-archive-fill text-primary me-2"></i>백업 및 저장소
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 백업된 파일 목록 #}
|
||||
<div class="row backup-list">
|
||||
<div class="col">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light border-0 py-2">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-archive me-2"></i>
|
||||
백업된 파일 목록
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if backup_files and backup_files|length > 0 %}
|
||||
<div class="list-group">
|
||||
{% for date, info in backup_files.items() %}
|
||||
<div class="list-group-item border rounded mb-2 p-0 overflow-hidden">
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-calendar3 text-primary me-2"></i>
|
||||
<strong>{{ date }}</strong>
|
||||
<span class="badge bg-primary ms-3">{{ info.count }} 파일</span>
|
||||
<div class="custom-tabs-container" id="fileTabsContent">
|
||||
|
||||
{# ===================== [TAB 1] 처리된 파일 ===================== #}
|
||||
<div class="custom-tab-panel custom-active" id="processed" role="tabpanel" aria-labelledby="processed-tab">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>현재 스테이징 폴더(data/temp/staging)에
|
||||
있는 파일들입니다.</h6>
|
||||
<div class="input-group input-group-sm w-auto">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control border-start-0" id="searchProcessed" placeholder="파일 검색..."
|
||||
onkeyup="filterTable('tableProcessed', this.value)">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-{{ loop.index }}" aria-expanded="false">
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="collapse-{{ loop.index }}" class="collapse">
|
||||
<div class="p-3">
|
||||
<div class="row g-3 backup-files-container" data-folder="{{ date }}" style="min-height: 50px;">
|
||||
{% for file in info.files %}
|
||||
<div class="col-auto backup-file-item" data-filename="{{ file }}">
|
||||
<div class="file-card-compact border rounded p-2 text-center bg-white">
|
||||
<a href="{{ url_for('main.download_backup_file', date=date, filename=file) }}"
|
||||
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2" download
|
||||
title="{{ file }}">
|
||||
{{ file.rsplit('.', 1)[0] }}
|
||||
</a>
|
||||
<div class="file-card-single-button">
|
||||
<button type="button" class="btn btn-sm btn-outline btn-view-backup w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="backup"
|
||||
data-date="{{ date }}" data-filename="{{ file }}">
|
||||
보기
|
||||
|
||||
<div class="table-responsive bg-white border rounded">
|
||||
<table class="table table-hover align-middle mb-0" id="tableProcessed">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-3" style="cursor:pointer;" onclick="sortTable('tableProcessed', 0)">파일명 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th style="width: 150px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if files_to_display and files_to_display|length > 0 %}
|
||||
{% for file_info in files_to_display %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-file-earmark-text text-secondary fs-5 me-3"></i>
|
||||
<div>
|
||||
<a href="{{ url_for('main.download_file', filename=file_info.file) }}"
|
||||
class="text-decoration-none fw-semibold text-dark" download>
|
||||
{{ file_info.name or file_info.file }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-light text-primary bg-primary-subtle border-0"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="idrac_info"
|
||||
data-filename="{{ file_info.file }}" title="내용 보기">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
<form action="{{ url_for('main.delete_file', filename=file_info.file) }}" method="post"
|
||||
class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-light text-danger bg-danger-subtle border-0"
|
||||
onclick="return confirm('이 파일을 삭제하시겠습니까?');" title="삭제">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">표시할 파일이 없습니다.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# 페이지네이션 (기존 로직 유지) #}
|
||||
{% if total_pages > 1 %}
|
||||
<nav aria-label="Processed files pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center mb-0 pagination-sm">
|
||||
{% if page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.index', page=page-1) }}"><i
|
||||
class="bi bi-chevron-left"></i></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link"><i class="bi bi-chevron-left"></i></span></li>
|
||||
{% endif %}
|
||||
{% set start_page = ((page - 1) // 10) * 10 + 1 %}
|
||||
{% set end_page = [start_page + 9, total_pages]|min %}
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('main.index', page=p) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if page < total_pages %} <li class="page-item"><a class="page-link"
|
||||
href="{{ url_for('main.index', page=page+1) }}"><i class="bi bi-chevron-right"></i></a></li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ===================== [TAB 2] 백업 및 저장소 ===================== #}
|
||||
<div class="custom-tab-panel" id="repository-panel" role="tabpanel" aria-labelledby="repository-tab"
|
||||
style="display: none !important;">
|
||||
|
||||
{# 서브 탭 (Pills) #}
|
||||
<ul class="nav nav-pills mb-3 gap-2" id="repoTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active btn-sm" id="subpanel-backup-tab" data-custom-toggle="pill"
|
||||
data-bs-target="#subpanel-backup" type="button" role="tab" aria-selected="true">
|
||||
<i class="bi bi-hdd-server me-1"></i>서버 정보
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link btn-sm" id="subpanel-mac-tab" data-custom-toggle="pill"
|
||||
data-bs-target="#subpanel-mac" type="button" role="tab" aria-selected="false">
|
||||
<i class="bi bi-cpu me-1"></i>MAC
|
||||
<span class="badge bg-light text-dark ms-1">{{ mac_files|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link btn-sm" id="subpanel-guid-tab" data-custom-toggle="pill"
|
||||
data-bs-target="#subpanel-guid" type="button" role="tab" aria-selected="false">
|
||||
<i class="bi bi-fingerprint me-1"></i>GUID
|
||||
<span class="badge bg-light text-dark ms-1">{{ guid_files|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link btn-sm" id="subpanel-gpu-tab" data-custom-toggle="pill"
|
||||
data-bs-target="#subpanel-gpu" type="button" role="tab" aria-selected="false">
|
||||
<i class="bi bi-gpu-card me-1"></i>GPU
|
||||
<span class="badge bg-light text-dark ms-1">{{ gpu_files|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="custom-sub-tabs-container" id="repoTabsContent">
|
||||
|
||||
{# ----- 서브탭 1: 일별 백업 (기존 아코디언 스타일 유지 + 파일 목록 테이블화) ----- #}
|
||||
<div class="custom-sub-panel" id="subpanel-backup" role="tabpanel">
|
||||
{% if server_info_files and server_info_files|length > 0 %}
|
||||
<div class="accordion" id="backupAccordion">
|
||||
{% for folder_name, info in server_info_files.items() %}
|
||||
<div class="accordion-item mb-2 border rounded overflow-hidden">
|
||||
<h2 class="accordion-header" id="heading-{{ loop.index }}">
|
||||
<div class="d-flex align-items-center bg-light w-100 px-0">
|
||||
<button class="accordion-button collapsed bg-light py-2 shadow-none border-0" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapse-{{ loop.index }}" aria-expanded="false"
|
||||
style="width: auto; flex-grow: 1;">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-hdd-server text-primary me-2"></i>
|
||||
<span class="fw-semibold me-3">{{ folder_name
|
||||
}}</span>
|
||||
<span class="badge bg-secondary rounded-pill">{{ info.count }} 파일</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{# 폴더 액션 버튼 (버튼 밖으로 이동하여 HTML 표준 준수) #}
|
||||
<div class="d-flex gap-2 pe-3 me-4">
|
||||
<form action="{{ url_for('main.archive_backup_folder', folder_name=folder_name) }}"
|
||||
method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-xs btn-outline-secondary" title="아카이브로 이동"
|
||||
onclick="return confirm('아카이브하시겠습니까?');">
|
||||
<i class="bi bi-archive"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('main.delete_backup_folder', folder_name=folder_name) }}"
|
||||
method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-xs btn-outline-danger" title="폴더 삭제"
|
||||
onclick="return confirm('영구 삭제하시겠습니까?');">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="collapse-{{ loop.index }}" class="accordion-collapse collapse">
|
||||
<div class="accordion-body p-0">
|
||||
{# 백업 파일 테이블 (내부 검색/정렬은 생략하거나 필요시 추가) #}
|
||||
<div class="table-responsive" style="max-height: 400px;">
|
||||
<table class="table table-sm table-hover mb-0 font-monospace" style="font-size: 0.9rem;">
|
||||
<thead class="bg-light sticky-top">
|
||||
<tr>
|
||||
<th class="ps-3">파일명</th>
|
||||
<th class="text-end pe-3" style="width: 100px;">보기</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="backup-files-container" data-folder="{{ folder_name }}">
|
||||
{% for file in info.files %}
|
||||
<tr class="backup-file-item" data-filename="{{ file }}">
|
||||
<td class="ps-3 align-middle">
|
||||
<a href="{{ url_for('main.download_backup_file', date=folder_name, filename=file) }}"
|
||||
class="text-decoration-none text-dark d-block text-truncate" download>
|
||||
{{ file }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<button type="button" class="btn btn-xs btn-link text-secondary p-0 btn-view-backup"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="server_info"
|
||||
data-date="{{ folder_name }}" data-filename="{{ file }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# 백업 페이지네이션 #}
|
||||
{% if total_backup_pages > 1 %}
|
||||
<nav class="mt-3">
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
{% if backup_page > 1 %}
|
||||
<li class="page-item"><a class="page-link"
|
||||
href="{{ url_for('main.index', backup_page=backup_page-1, page=page) }}"><i
|
||||
class="bi bi-chevron-left"></i></a></li>
|
||||
{% endif %}
|
||||
{% set start_b = ((backup_page - 1) // 10) * 10 + 1 %}
|
||||
{% set end_b = [start_b + 9, total_backup_pages]|min %}
|
||||
{% for p in range(start_b, end_b + 1) %}
|
||||
<li class="page-item {% if p == backup_page %}active{% endif %}"><a class="page-link"
|
||||
href="{{ url_for('main.index', backup_page=p, page=page) }}">{{ p }}</a></li>
|
||||
{% endfor %}
|
||||
{% if backup_page < total_backup_pages %} <li class="page-item"><a class="page-link"
|
||||
href="{{ url_for('main.index', backup_page=backup_page+1, page=page) }}"><i
|
||||
class="bi bi-chevron-right"></i></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">백업된 파일이 없습니다.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ----- 서브탭 2: MAC ----- #}
|
||||
<div class="custom-sub-panel d-none" id="subpanel-mac" role="tabpanel">
|
||||
{% with table_id="tableMac", files=mac_files, folder="mac", empty_msg="MAC 파일이 없습니다." %}
|
||||
{% include "snippets/repo_file_table.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# ----- 서브탭 3: GUID ----- #}
|
||||
<div class="custom-sub-panel d-none" id="subpanel-guid" role="tabpanel">
|
||||
{% with table_id="tableGuid", files=guid_files, folder="guid", empty_msg="GUID 파일이 없습니다." %}
|
||||
{% include "snippets/repo_file_table.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# ----- 서브탭 4: GPU ----- #}
|
||||
<div class="custom-sub-panel d-none" id="subpanel-gpu" role="tabpanel">
|
||||
{% with table_id="tableGpu", files=gpu_files, folder="gpu", empty_msg="GPU 파일이 없습니다." %}
|
||||
{% include "snippets/repo_file_table.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div> <!-- End Tab 2 -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 백업 목록 페이지네이션 -->
|
||||
{% if total_backup_pages > 1 %}
|
||||
<nav aria-label="Backup pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
|
||||
<!-- 이전 페이지 -->
|
||||
{% if backup_page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.index', backup_page=backup_page-1, page=page) }}">
|
||||
<i class="bi bi-chevron-left"></i> 이전
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i> 이전</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- 페이지 번호 -->
|
||||
{% set start_b_page = ((backup_page - 1) // 10) * 10 + 1 %}
|
||||
{% set end_b_page = [start_b_page + 9, total_backup_pages]|min %}
|
||||
|
||||
{% for p in range(start_b_page, end_b_page + 1) %}
|
||||
<li class="page-item {% if p == backup_page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('main.index', backup_page=p, page=page) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 다음 페이지 -->
|
||||
{% if backup_page < total_backup_pages %} <li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.index', backup_page=backup_page+1, page=page) }}">
|
||||
다음 <i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">다음 <i class="bi bi-chevron-right"></i></span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">백업된 파일이 없습니다.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{# 파일 보기 모달 #}
|
||||
@@ -547,11 +802,53 @@
|
||||
window.APP_CONFIG = {
|
||||
moveBackupUrl: "{{ url_for('main.move_backup_files') }}",
|
||||
csrfToken: "{{ csrf_token() }}",
|
||||
downloadBaseUrl: "{{ url_for('main.download_backup_file', date='PLACEHOLDER_DATE', filename='PLACEHOLDER_FILE') }}"
|
||||
downloadBaseUrl: "{{ url_for('main.download_backup_file', date='PLACEHOLDER_DATE', filename='PLACEHOLDER_FILE') }}",
|
||||
systemControlUrl: "{{ url_for('utils.system_control') }}"
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v={{ range(1, 100000) | random }}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
async function runControl(action) {
|
||||
// 메인 IP 입력란('ips')에서 값을 가져옴
|
||||
const ipText = document.getElementById("ips").value.trim();
|
||||
if (!ipText) {
|
||||
alert("IP 처리에 입력된 IP 주소가 없습니다.\n먼저 'IP 처리' 카드에 IP를 입력해주세요.");
|
||||
document.getElementById("ips").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`정말로 ${action} 작업을 진행하시겠습니까?`)) return;
|
||||
|
||||
// Simple loading indicator (can be improved)
|
||||
const btn = event.target.closest('button');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 처리중...';
|
||||
|
||||
try {
|
||||
const response = await axios.post(window.APP_CONFIG.systemControlUrl, {
|
||||
action: action,
|
||||
ips: ipText.split("\n").filter(ip => ip.trim() !== "")
|
||||
}, {
|
||||
headers: {
|
||||
'X-CSRFToken': window.APP_CONFIG.csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
alert("작업 완료: " + response.data.message);
|
||||
} else {
|
||||
alert("오류 발생: " + response.data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("서버 통신 오류: " + (error.response?.data?.error || error.message));
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- SortableJS for Drag and Drop -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||
|
||||
@@ -98,6 +98,26 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 테이블 뷰 스타일 */
|
||||
#xmlFileTable {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.file-icon-wrapper-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #eff6ff;
|
||||
color: #3b82f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -215,58 +235,56 @@
|
||||
<div class="card-body bg-light">
|
||||
{% if xml_files %}
|
||||
<!-- 카드 크기 조정: 한 줄에 4개(xxl), 3개(xl) 등으로 조금 더 키움 -->
|
||||
<div class="row row-cols-1 row-cols-lg-2 row-cols-xl-3 row-cols-xxl-4 g-3">
|
||||
{% for xml_file in xml_files %}
|
||||
<div class="col">
|
||||
<div class="xml-file-card position-relative p-3 h-100 d-flex flex-column">
|
||||
<div class="position-absolute top-0 end-0 p-2 me-1">
|
||||
<input type="checkbox" class="form-check-input file-selector border-secondary"
|
||||
value="{{ xml_file }}" style="cursor: pointer;">
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="file-icon-wrapper me-3 mb-0 shadow-sm"
|
||||
style="width: 42px; height: 42px; font-size: 1.4rem;">
|
||||
<i class="bi bi-filetype-xml"></i>
|
||||
</div>
|
||||
<div class="file-name text-truncate fw-bold mb-0 text-dark"
|
||||
style="max-width: 140px; font-size: 0.95rem;" title="{{ xml_file }}">
|
||||
{{ xml_file }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-3 border-top">
|
||||
<div class="d-flex gap-2">
|
||||
<!-- 배포 버튼 -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-primary flex-fill d-flex align-items-center justify-content-center gap-1"
|
||||
onclick="openDeployModal('{{ xml_file }}')" title="배포">
|
||||
<i class="bi bi-send-fill"></i> <span class="small fw-bold">배포</span>
|
||||
</button>
|
||||
|
||||
<!-- 편집 버튼 -->
|
||||
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}"
|
||||
class="btn btn-sm btn-white border flex-fill d-flex align-items-center justify-content-center gap-1 text-dark bg-white"
|
||||
title="편집">
|
||||
<i class="bi bi-pencil-fill text-secondary"></i> <span
|
||||
class="small fw-bold">편집</span>
|
||||
</a>
|
||||
|
||||
<!-- 삭제 버튼 -->
|
||||
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST"
|
||||
class="d-flex flex-fill m-0" style="min-width: 0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-white border w-100 d-flex align-items-center justify-content-center gap-1 text-danger bg-white"
|
||||
onclick="return confirm('정말 삭제하시겠습니까?')" title="삭제">
|
||||
<i class="bi bi-trash-fill"></i> <span class="small fw-bold">삭제</span>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm align-middle mb-0" id="xmlFileTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 50px;" class="text-center">
|
||||
<input type="checkbox" class="form-check-input" id="checkAll">
|
||||
</th>
|
||||
<th scope="col">파일명</th>
|
||||
<th scope="col" style="width: 180px;" class="text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for xml_file in xml_files %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input file-selector"
|
||||
value="{{ xml_file }}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="file-icon-wrapper-sm me-3">
|
||||
<i class="bi bi-filetype-xml fs-5"></i>
|
||||
</div>
|
||||
<span class="fw-bold text-dark">{{ xml_file }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="openDeployModal('{{ xml_file }}')" title="배포">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}"
|
||||
class="btn btn-sm btn-outline-secondary" title="편집">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</a>
|
||||
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}"
|
||||
method="POST" class="d-inline-block m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('정말 삭제하시겠습니까?')" title="삭제">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 my-5">
|
||||
@@ -519,6 +537,15 @@
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
});
|
||||
|
||||
// 전체 선택 체크박스 로직
|
||||
const checkAll = document.getElementById('checkAll');
|
||||
if (checkAll) {
|
||||
checkAll.addEventListener('change', function () {
|
||||
const checkboxes = document.querySelectorAll('.file-selector');
|
||||
checkboxes.forEach(cb => cb.checked = checkAll.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="input-group input-group-sm w-auto">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control border-start-0" placeholder="파일명 검색..."
|
||||
onkeyup="filterTable('{{ table_id }}', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive bg-white border rounded">
|
||||
<table class="table table-hover table-sm mb-0 align-middle font-monospace" id="{{ table_id }}"
|
||||
style="font-size: 0.9rem;">
|
||||
<thead class="bg-light sticky-top">
|
||||
<tr>
|
||||
<th class="ps-3" style="cursor:pointer;" onclick="sortTable('{{ table_id }}', 0)">파일명 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th style="cursor:pointer; width: 100px;" onclick="sortTable('{{ table_id }}', 1)">크기 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th style="cursor:pointer; width: 160px;" onclick="sortTable('{{ table_id }}', 2)">날짜 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th class="text-end pe-3" style="width: 80px;">보기</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if files and files|length > 0 %}
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<i class="bi bi-file-earmark text-secondary me-2"></i>{{ file.name }}
|
||||
</td>
|
||||
<td>{{ (file.size / 1024)|round(1) }} KB</td>
|
||||
<td data-sort="{{ file.mtime }}">{{ file.date }}</td>
|
||||
<td class="text-end pe-3">
|
||||
<button type="button" class="btn btn-xs btn-link text-secondary p-0 btn-view-repo"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="{{ folder }}"
|
||||
data-filename="{{ file.name }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">{{ empty_msg }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user