This commit is contained in:
unknown
2026-03-21 07:26:38 +09:00
parent 166d94fca4
commit 40150cd66d
406 changed files with 34179 additions and 55658 deletions
+721
View File
@@ -0,0 +1,721 @@
/**
* BIOS Baseline 비교 JavaScript
* backend/static/js/bios_baseline.js
*/
// 전역 변수
let baselines = [];
let comparisonResults = [];
let showDiffOnly = true;
// ========================================
// 초기화
// ========================================
document.addEventListener('DOMContentLoaded', () => {
loadBaselines();
// IP 입력 카운트 업데이트
document.getElementById('ip-addresses').addEventListener('input', updateIPCount);
// Baseline 생성 폼 제출
document.getElementById('create-baseline-form').addEventListener('submit', handleCreateBaseline);
});
// ========================================
// Baseline 관리
// ========================================
async function loadBaselines() {
try {
const res = await fetch('/bios-baseline/api/baselines');
const data = await res.json();
if (data.success) {
baselines = data.baselines;
renderBaselineSelect();
}
} catch (e) {
console.error('Baseline 로드 실패:', e);
}
}
function renderBaselineSelect() {
const select = document.getElementById('baseline-select');
select.innerHTML = '<option value="">Baseline을 선택하세요</option>';
baselines.forEach(baseline => {
const option = document.createElement('option');
option.value = baseline.id;
option.textContent = `${baseline.name} (${baseline.server_model})`;
select.appendChild(option);
});
}
function showBaselineManageModal() {
const modal = new bootstrap.Modal(document.getElementById('baseline-manage-modal'));
modal.show();
loadBaselineList();
}
async function loadBaselineList() {
const container = document.getElementById('baseline-list');
container.innerHTML = '<div class="text-center text-muted py-4"><i class="bi bi-hourglass-split"></i> 로딩 중...</div>';
try {
const res = await fetch('/bios-baseline/api/baselines');
const data = await res.json();
if (data.success && data.baselines.length > 0) {
let html = '<div class="list-group">';
data.baselines.forEach(baseline => {
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">${baseline.name}</h6>
<p class="mb-1 text-muted small">
<i class="bi bi-server me-1"></i>${baseline.server_model}
${baseline.server_type ? `<span class="badge bg-secondary ms-2">${baseline.server_type}</span>` : ''}
</p>
<p class="mb-0 text-muted small">
${baseline.description || '설명 없음'}
</p>
<p class="mb-0 text-muted small">
<i class="bi bi-gear me-1"></i>${baseline.total_settings}개 설정
<i class="bi bi-calendar ms-2 me-1"></i>${new Date(baseline.created_at).toLocaleDateString()}
</p>
</div>
<div class="btn-group-vertical btn-group-sm">
<button class="btn btn-outline-secondary" onclick="editBaseline(${baseline.id})">
<i class="bi bi-pencil"></i> 편집
</button>
<button class="btn btn-outline-primary" onclick="duplicateBaseline(${baseline.id})">
<i class="bi bi-files"></i> 복사
</button>
<button class="btn btn-outline-danger" onclick="deleteBaseline(${baseline.id})">
<i class="bi bi-trash"></i> 삭제
</button>
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
} else {
container.innerHTML = '<div class="text-center text-muted py-4">등록된 Baseline이 없습니다</div>';
}
} catch (e) {
container.innerHTML = '<div class="text-center text-danger py-4">로드 실패</div>';
}
}
async function handleCreateBaseline(e) {
e.preventDefault();
const data = {
ip_address: document.getElementById('create-ip').value,
username: document.getElementById('create-username').value,
password: document.getElementById('create-password').value,
name: document.getElementById('create-name').value,
server_type: document.getElementById('create-type').value,
description: document.getElementById('create-description').value,
created_by: 'admin'
};
try {
const res = await fetch('/bios-baseline/api/baselines/create-from-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
alert('✓ Baseline 생성 완료!');
document.getElementById('create-baseline-form').reset();
loadBaselines();
loadBaselineList();
} else {
alert('✗ 생성 실패: ' + result.message);
}
} catch (e) {
alert('✗ 오류: ' + e.message);
}
}
async function duplicateBaseline(baselineId) {
const newName = prompt('새 Baseline 이름을 입력하세요:');
if (!newName) return;
try {
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}/duplicate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ new_name: newName })
});
const result = await res.json();
if (result.success) {
alert('✓ Baseline 복사 완료!');
loadBaselines();
loadBaselineList();
} else {
alert('✗ 복사 실패: ' + result.message);
}
} catch (e) {
alert('✗ 오류: ' + e.message);
}
}
// Baseline 삭제
async function deleteBaseline(baselineId) {
if (!confirm('정말 이 Baseline을 삭제하시겠습니까?')) return;
try {
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result.success) {
alert('✓ Baseline 삭제 완료!');
loadBaselines();
loadBaselineList();
} else {
alert('✗ 삭제 실패: ' + result.message);
}
} catch (e) {
alert('✗ 오류: ' + e.message);
}
}
// ========================================
// Baseline 상세 편집
// ========================================
async function editBaseline(baselineId) {
try {
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}`);
const data = await res.json();
if (data.success) {
const baseline = data.baseline;
currentBaselineId = baselineId; // 전역 변수 설정
// 메타데이터 채우기
document.getElementById('edit-id').value = baseline.id;
document.getElementById('edit-name').value = baseline.name;
document.getElementById('edit-model').value = baseline.server_model;
document.getElementById('edit-type').value = baseline.server_type || '';
document.getElementById('edit-description').value = baseline.description || '';
document.getElementById('edit-created-at').textContent = baseline.created_at || '-';
document.getElementById('edit-source-ip').textContent = baseline.source_ip || '-';
// BIOS, iDRAC, Firmware Table 렌더링
renderEditTable('bios', baseline.bios_data || {});
renderEditTable('idrac', baseline.idrac_data || {});
renderEditTable('firmware', baseline.firmware_data || {});
// RAID, NIC, Boot는 JSON Textarea에 표시
document.getElementById('json-edit-raid').value = JSON.stringify(baseline.raid_data || {}, null, 4);
document.getElementById('json-edit-nic').value = JSON.stringify(baseline.nic_data || {}, null, 4);
document.getElementById('json-edit-boot').value = JSON.stringify(baseline.boot_data || {}, null, 4);
// 모달 표시
const modal = new bootstrap.Modal(document.getElementById('edit-baseline-modal'));
modal.show();
// 첫 번째 탭(BIOS) 활성화
const firstTab = new bootstrap.Tab(document.querySelector('#tab-edit-bios'));
firstTab.show();
} else {
alert('로드 실패: ' + data.message);
}
} catch (e) {
alert('오류: ' + e.message);
}
}
function renderEditTable(type, settings) {
const tbody = document.querySelector(`#table-edit-${type} tbody`);
tbody.innerHTML = '';
const entries = Object.entries(settings).sort((a, b) => a[0].localeCompare(b[0]));
document.getElementById(`edit-${type}-count`).textContent = entries.length;
entries.forEach(([key, value]) => {
addSettingRow(type, key, value);
});
}
function addSettingRow(type, key = '', value = '') {
const tbody = document.querySelector(`#table-edit-${type} tbody`);
const tr = document.createElement('tr');
tr.innerHTML = `
<td><input type="text" class="form-control form-control-sm setting-key" value="${key}" placeholder="Key"></td>
<td><input type="text" class="form-control form-control-sm setting-value" value="${value}" placeholder="Value"></td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="removeSettingRow(this, '${type}')">
<i class="bi bi-trash"></i>
</button>
</td>
`;
// 새 행은 맨 위에 추가 (비어있을 경우) 혹은 맨 아래
if (!key) {
tbody.prepend(tr);
tr.querySelector('.setting-key').focus();
} else {
tbody.appendChild(tr);
}
}
function removeSettingRow(btn, type) {
btn.closest('tr').remove();
updateSettingsCount(type);
}
function updateSettingsCount(type) {
const count = document.querySelectorAll(`#table-edit-${type} tbody tr`).length;
document.getElementById(`edit-${type}-count`).textContent = count;
}
async function saveBaselineChanges() {
const id = document.getElementById('edit-id').value;
const name = document.getElementById('edit-name').value;
if (!name) {
alert('Baseline 이름을 입력하세요.');
return;
}
// Helper function to collect table data
const collectTableData = (type) => {
const data = {};
const rows = document.querySelectorAll(`#table-edit-${type} tbody tr`);
rows.forEach(row => {
const key = row.querySelector('.setting-key').value.trim();
const value = row.querySelector('.setting-value').value.trim();
if (key) data[key] = value;
});
return data;
};
const biosData = collectTableData('bios');
const idracData = collectTableData('idrac');
const firmwareData = collectTableData('firmware');
// JSON Data Parsing
let raidData = {};
let nicData = {};
let bootData = {};
try {
raidData = JSON.parse(document.getElementById('json-edit-raid').value || '{}');
} catch (e) {
alert('RAID 구성 JSON 형식이 올바르지 않습니다.');
return;
}
try {
nicData = JSON.parse(document.getElementById('json-edit-nic').value || '{}');
} catch (e) {
alert('NIC 설정 JSON 형식이 올바르지 않습니다.');
return;
}
try {
bootData = JSON.parse(document.getElementById('json-edit-boot').value || '{}');
} catch (e) {
alert('부팅 설정 JSON 형식이 올바르지 않습니다.');
return;
}
const payload = {
name: name,
server_type: document.getElementById('edit-type').value,
description: document.getElementById('edit-description').value,
bios_data: biosData,
idrac_data: idracData,
raid_data: raidData,
nic_data: nicData,
boot_data: bootData,
firmware_data: firmwareData
};
try {
const res = await fetch(`/bios-baseline/api/baselines/${currentBaselineId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
alert('✓ 변경사항이 저장되었습니다!');
const modalEl = document.getElementById('edit-baseline-modal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
loadBaselines();
loadBaselineList();
} else {
alert('✗ 저장 실패: ' + result.message);
}
} catch (e) {
alert('✗ 오류: ' + e.message);
}
}
// ========================================
// 비교 기능
// ========================================
async function startComparison() {
const baselineId = document.getElementById('baseline-select').value;
const ipText = document.getElementById('ip-addresses').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!baselineId) {
alert('Baseline을 선택하세요');
return;
}
if (!ipText.trim()) {
alert('서버 IP를 입력하세요');
return;
}
if (!password) {
alert('비밀번호를 입력하세요');
return;
}
// IP 파싱
const ipAddresses = ipText.split(/[\n,\s]+/).filter(ip => ip.trim());
if (ipAddresses.length === 0) {
alert('유효한 IP 주소가 없습니다');
return;
}
// 진행률 표시
document.getElementById('progress-section').style.display = 'block';
document.getElementById('results-section').style.display = 'none';
updateProgress(0, ipAddresses.length);
try {
const res = await fetch('/bios-baseline/api/compare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ip_addresses: ipAddresses,
baseline_id: parseInt(baselineId),
username: username,
password: password
})
});
const data = await res.json();
if (data.success) {
comparisonResults = data.results;
// Toggle 버튼 초기 상태 설정
const toggleBtn = document.getElementById('diff-only-btn');
if (showDiffOnly) {
toggleBtn.classList.add('active');
} else {
toggleBtn.classList.remove('active');
}
renderSummary(data.baseline, data.results);
renderResults(data.results);
document.getElementById('results-section').style.display = 'block';
} else {
alert('비교 실패: ' + data.message);
}
} catch (e) {
alert('오류: ' + e.message);
} finally {
document.getElementById('progress-section').style.display = 'none';
}
}
function updateProgress(current, total) {
const percent = Math.round((current / total) * 100);
const bar = document.getElementById('progress-bar');
bar.style.width = percent + '%';
bar.querySelector('span').textContent = percent + '%';
}
function renderSummary(baseline, results) {
const successResults = results.filter(r => r.status === 'success');
const totalServers = results.length;
const successCount = successResults.length;
const failedCount = totalServers - successCount;
let totalMatched = 0;
let totalMismatched = 0;
let perfectMatchCount = 0;
successResults.forEach(r => {
totalMatched += r.matched_items;
totalMismatched += r.mismatched_items;
if (r.mismatched_items === 0) perfectMatchCount++;
});
const avgMatchRate = successCount > 0
? Math.round((totalMatched / (totalMatched + totalMismatched)) * 100)
: 0;
const html = `
<div class="row g-3">
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<i class="bi bi-server fs-1 text-primary"></i>
<h3 class="mt-2 mb-0">${totalServers}</h3>
<p class="text-muted small mb-0">총 서버</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success">
<div class="card-body text-center">
<i class="bi bi-check-circle fs-1 text-success"></i>
<h3 class="mt-2 mb-0">${avgMatchRate}%</h3>
<p class="text-muted small mb-0">평균 일치율</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-warning">
<div class="card-body text-center">
<i class="bi bi-exclamation-triangle fs-1 text-warning"></i>
<h3 class="mt-2 mb-0">${totalMismatched}</h3>
<p class="text-muted small mb-0">불일치 항목</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-info">
<div class="card-body text-center">
<i class="bi bi-patch-check fs-1 text-info"></i>
<h3 class="mt-2 mb-0">${perfectMatchCount}</h3>
<p class="text-muted small mb-0">완전 일치</p>
</div>
</div>
</div>
</div>
<div class="mt-3 p-3 bg-light rounded">
<p class="mb-0 small">
<strong>Baseline:</strong> ${baseline.name} (${baseline.server_model})
<span class="ms-3"><strong>총 설정:</strong> ${baseline.total_settings}개</span>
</p>
</div>
`;
document.getElementById('summary-section').innerHTML = html;
}
function renderResults(results) {
const container = document.getElementById('results-container');
if (results.length === 0) {
container.innerHTML = '<p class="text-muted text-center">결과가 없습니다</p>';
return;
}
let html = '<div class="accordion" id="results-accordion">';
results.forEach((result, index) => {
const statusClass = result.status === 'success'
? (result.mismatched_items === 0 ? 'success' : 'warning')
: 'danger';
const statusIcon = result.status === 'success'
? (result.mismatched_items === 0 ? 'check-circle-fill' : 'exclamation-triangle-fill')
: 'x-circle-fill';
html += `
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button ${index > 0 ? 'collapsed' : ''}" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse-${index}">
<i class="bi bi-${statusIcon} text-${statusClass} me-2"></i>
<strong>${result.ip}</strong>
${result.service_tag ? `<span class="badge bg-secondary ms-2">${result.service_tag}</span>` : ''}
${result.status === 'success' ? `
<span class="ms-auto me-3">
<span class="badge bg-${statusClass}">${result.match_rate}% 일치</span>
${result.mismatched_items > 0 ? `<span class="badge bg-danger ms-1">${result.mismatched_items}개 불일치</span>` : ''}
</span>
` : ''}
</button>
</h2>
<div id="collapse-${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
data-bs-parent="#results-accordion">
<div class="accordion-body">
${renderServerDetail(result)}
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function renderServerDetail(result) {
if (result.status !== 'success') {
return `<div class="alert alert-danger mb-0">${result.message}</div>`;
}
// 통계바
let html = `
<div class="mb-3">
<div class="row g-2">
<div class="col-md-4"><strong>모델:</strong> ${result.server_model}</div>
<div class="col-md-4"><strong>Service Tag:</strong> ${result.service_tag}</div>
<div class="col-md-4"><strong>BIOS:</strong> ${result.bios_version}</div>
</div>
<div class="progress mt-2" style="height: 20px;">
<div class="progress-bar bg-success" role="progressbar" style="width: ${result.match_rate}%">
${result.match_rate}% 일치
</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: ${100 - result.match_rate}%">
${result.mismatched_items} 불일치
</div>
</div>
</div>
`;
// 탭 헤더 생성 (Nav Pills)
const uid = Math.random().toString(36).substr(2, 9); // 유니크 ID
html += `
<div class="nav-pills-container">
<ul class="nav nav-pills mb-0" id="result-tabs-${uid}" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-bios-${uid}" type="button">BIOS</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-idrac-${uid}" type="button">iDRAC</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-raid-${uid}" type="button">RAID</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-nic-${uid}" type="button">NIC</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-boot-${uid}" type="button">Boot</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-firmware-${uid}" type="button">Firmware</button>
</li>
</ul>
</div>
<div class="tab-content">
`;
// 섹션별 Diff 필터링
const diffs = result.differences || [];
// 탭 콘텐츠 생성 Helper
const renderTabPane = (section, id, isActive = false) => {
const sectionDiffs = diffs.filter(d => d.section === section);
// 섹션 이름이 영어로 저장되어 있다고 가정 (BIOS, iDRAC, RAID, NIC, Boot, Firmware)
return `
<div class="tab-pane fade ${isActive ? 'show active' : ''}" id="tab-${id}-${uid}" role="tabpanel">
${renderDiffTable(sectionDiffs, `${section} 설정이 기준과 일치합니다.`)}
</div>`;
};
html += renderTabPane('BIOS', 'bios', true);
html += renderTabPane('iDRAC', 'idrac');
html += renderTabPane('RAID', 'raid');
html += renderTabPane('NIC', 'nic');
html += renderTabPane('Boot', 'boot');
html += renderTabPane('Firmware', 'firmware');
html += `</div>`; // tab-content end
return html;
}
function renderDiffTable(diffs, emptyMsg) {
if (diffs.length === 0) {
return `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i> ${emptyMsg}</div>`;
}
if (showDiffOnly && diffs.every(d => d.status !== 'mismatch')) {
return `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i> ${emptyMsg} (필터링됨)</div>`;
}
let html = `
<table class="table table-sm table-bordered table-hover" style="table-layout: fixed;">
<thead class="table-light">
<tr>
<th width="40%">항목</th>
<th width="30%">기준값 (Baseline)</th>
<th width="30%">실제값 (Server)</th>
</tr>
</thead>
<tbody>
`;
diffs.forEach(diff => {
const isMismatch = diff.status === 'mismatch';
const rowClass = isMismatch ? 'table-danger' : '';
if (!showDiffOnly || isMismatch) {
html += `
<tr class="${rowClass}">
<td style="word-wrap: break-word;">
${diff.display_name ? `<strong>${diff.display_name}</strong><br><small class="text-muted" style="word-break: break-all;">${diff.setting_name}</small>` : `<code style="word-break: break-all;">${diff.setting_name}</code>`}
</td>
<td style="word-break: break-all;">${diff.expected}</td>
<td style="word-break: break-all;"><span class="${isMismatch ? '' : 'text-success'}">${diff.actual}</span></td>
</tr>
`;
}
});
html += '</tbody></table>';
return html;
}
function toggleDiffOnly() {
showDiffOnly = !showDiffOnly;
const btn = document.getElementById('diff-only-btn');
btn.classList.toggle('active');
renderResults(comparisonResults);
}
function downloadResults() {
alert('Excel 다운로드 기능은 곧 추가될 예정입니다.');
}
// ========================================
// 유틸리티
// ========================================
function updateIPCount() {
const ipText = document.getElementById('ip-addresses').value;
const ips = ipText.split(/[\n,\s]+/).filter(ip => ip.trim());
document.getElementById('ip-count').textContent = ips.length;
}
function clearIPs() {
document.getElementById('ip-addresses').value = '';
updateIPCount();
}