/** * 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 = ''; 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 = '
로딩 중...
'; try { const res = await fetch('/bios-baseline/api/baselines'); const data = await res.json(); if (data.success && data.baselines.length > 0) { let html = '
'; data.baselines.forEach(baseline => { html += `
${baseline.name}

${baseline.server_model} ${baseline.server_type ? `${baseline.server_type}` : ''}

${baseline.description || '설명 없음'}

${baseline.total_settings}개 설정 ${new Date(baseline.created_at).toLocaleDateString()}

`; }); html += '
'; container.innerHTML = html; } else { container.innerHTML = '
등록된 Baseline이 없습니다
'; } } catch (e) { container.innerHTML = '
로드 실패
'; } } 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 = ` `; // 새 행은 맨 위에 추가 (비어있을 경우) 혹은 맨 아래 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 = `

${totalServers}

총 서버

${avgMatchRate}%

평균 일치율

${totalMismatched}

불일치 항목

${perfectMatchCount}

완전 일치

Baseline: ${baseline.name} (${baseline.server_model}) 총 설정: ${baseline.total_settings}개

`; document.getElementById('summary-section').innerHTML = html; } function renderResults(results) { const container = document.getElementById('results-container'); if (results.length === 0) { container.innerHTML = '

결과가 없습니다

'; return; } let html = '
'; 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 += `

${renderServerDetail(result)}
`; }); html += '
'; container.innerHTML = html; } function renderServerDetail(result) { if (result.status !== 'success') { return `
${result.message}
`; } // 통계바 let html = `
모델: ${result.server_model}
Service Tag: ${result.service_tag}
BIOS: ${result.bios_version}
${result.match_rate}% 일치
${result.mismatched_items} 불일치
`; // 탭 헤더 생성 (Nav Pills) const uid = Math.random().toString(36).substr(2, 9); // 유니크 ID html += `
`; // 섹션별 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 `
${renderDiffTable(sectionDiffs, `${section} 설정이 기준과 일치합니다.`)}
`; }; 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 += `
`; // tab-content end return html; } function renderDiffTable(diffs, emptyMsg) { if (diffs.length === 0) { return `
${emptyMsg}
`; } if (showDiffOnly && diffs.every(d => d.status !== 'mismatch')) { return `
${emptyMsg} (필터링됨)
`; } let html = ` `; diffs.forEach(diff => { const isMismatch = diff.status === 'mismatch'; const rowClass = isMismatch ? 'table-danger' : ''; if (!showDiffOnly || isMismatch) { html += ` `; } }); html += '
항목 기준값 (Baseline) 실제값 (Server)
${diff.display_name ? `${diff.display_name}
${diff.setting_name}` : `${diff.setting_name}`}
${diff.expected} ${diff.actual}
'; 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(); }