525 lines
23 KiB
HTML
525 lines
23 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}XML 설정 관리 & 배포 - Dell Server Info{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<!-- Existing SCP CSS for legacy support or specific components -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
|
|
<!-- Overriding/New Styles for Modern Look -->
|
|
<style>
|
|
/* 드래그 앤 드롭 영역 스타일 */
|
|
.drop-zone {
|
|
border: 2px dashed #cbd5e1;
|
|
border-radius: 12px;
|
|
background-color: #f8fafc;
|
|
transition: all 0.2s ease;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
.drop-zone:hover,
|
|
.drop-zone.dragover {
|
|
border-color: #3b82f6;
|
|
background-color: #eff6ff;
|
|
}
|
|
|
|
.drop-zone-icon {
|
|
font-size: 2.5rem;
|
|
color: #64748b;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.drop-zone-text {
|
|
font-weight: 500;
|
|
color: #334155;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.drop-zone-hint {
|
|
font-size: 0.875rem;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.drop-zone input[type="file"] {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* 카드 그리드 스타일 (index.html과 유사) */
|
|
.xml-file-card {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 10px;
|
|
padding: 1rem;
|
|
transition: all 0.2s ease;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.xml-file-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.file-icon-wrapper {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
|
color: #2563eb;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin-bottom: 0.5rem;
|
|
word-break: break-all;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.file-meta {
|
|
font-size: 0.75rem;
|
|
color: #64748b;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.btn-action {
|
|
flex: 1;
|
|
padding: 0.4rem;
|
|
font-size: 0.8rem;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin: 0;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
|
|
<!-- Header Section -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h2 class="fw-bold mb-1">
|
|
<i class="bi bi-file-earmark-code text-primary me-2"></i>
|
|
설정 파일 관리
|
|
</h2>
|
|
<p class="text-muted mb-0">서버 설정(XML) 파일을 업로드, 관리 및 배포합니다.</p>
|
|
</div>
|
|
<div class="col-auto align-self-end">
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
|
|
<i class="bi bi-server me-2"></i>iDRAC에서 추출
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Left: Upload Section (30% on large screens) -->
|
|
<div class="col-lg-4 col-xl-3">
|
|
<div class="card border shadow-sm h-100">
|
|
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
|
|
<h6 class="fw-bold mb-0 text-dark">
|
|
<i class="bi bi-cloud-upload me-2 text-primary"></i>파일 업로드
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data"
|
|
id="uploadForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
|
|
<div class="drop-zone" id="dropZone">
|
|
<input type="file" name="xmlFile" id="xmlFile" accept=".xml"
|
|
onchange="handleFileSelect(this)">
|
|
<div class="drop-zone-icon">
|
|
<i class="bi bi-file-earmark-arrow-up"></i>
|
|
</div>
|
|
<div class="drop-zone-text" id="dropZoneText">
|
|
클릭하여 파일 선택<br>또는 파일을 여기로 드래그
|
|
</div>
|
|
<div class="drop-zone-hint">XML 파일만 지원됩니다.</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary w-100 mt-3 shadow-sm">
|
|
<i class="bi bi-upload me-2"></i>업로드 시작
|
|
</button>
|
|
</form>
|
|
|
|
<div class="alert alert-light mt-4 border" role="alert">
|
|
<h6 class="alert-heading fs-6 fw-bold"><i class="bi bi-info-circle me-2"></i>도움말</h6>
|
|
<p class="mb-0 fs-small text-muted" style="font-size: 0.85rem;">
|
|
업로드된 XML 파일을 사용하여 여러 서버에 동일한 설정을 일괄 배포할 수 있습니다.
|
|
'비교' 기능을 사용하여 버전 간 차이를 확인하세요.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: File List (70%) -->
|
|
<div class="col-lg-8 col-xl-9">
|
|
<div class="card border shadow-sm h-100">
|
|
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center">
|
|
<h6 class="fw-bold mb-0 text-dark me-3">
|
|
<i class="bi bi-list-check me-2 text-success"></i>파일 목록
|
|
</h6>
|
|
<span class="badge bg-light text-dark border">{{ xml_files|length }}개 파일</span>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-secondary" id="compareBtn"
|
|
data-url="{{ url_for('scp.diff_scp') }}" onclick="compareSelected()">
|
|
<i class="bi bi-arrow-left-right me-1"></i>선택 비교
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5 my-5">
|
|
<div class="mb-3 text-secondary" style="font-size: 3rem; opacity: 0.3;">
|
|
<i class="bi bi-folder2-open"></i>
|
|
</div>
|
|
<h5 class="text-secondary fw-normal">등록된 파일이 없습니다.</h5>
|
|
<p class="text-muted">좌측 패널에서 XML 파일을 업로드해주세요.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Export Modal (Include existing modal logic but restyled) -->
|
|
<div class="modal fade" id="exportModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg">
|
|
<form action="{{ url_for('scp.export_scp') }}" method="POST">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-download me-2"></i>iDRAC 설정 내보내기</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-4">
|
|
<div class="alert alert-info py-2 small mb-4">
|
|
<i class="bi bi-info-circle-fill me-2"></i> CIFS 네트워크 공유 폴더가 필요합니다.
|
|
</div>
|
|
|
|
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
|
|
<div class="form-floating mb-2">
|
|
<input type="text" class="form-control" id="targetIp" name="target_ip" placeholder="IP"
|
|
required>
|
|
<label for="targetIp">iDRAC IP Address</label>
|
|
</div>
|
|
<div class="row g-2 mb-4">
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" id="targetUser" name="username"
|
|
placeholder="User" required>
|
|
<label for="targetUser">Username</label>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="password" class="form-control" id="targetPwd" name="password"
|
|
placeholder="Pwd" required>
|
|
<label for="targetPwd">Password</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="text-success fw-bold mb-3 small text-uppercase">저장소 (CIFS Share)</h6>
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-8">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
|
|
<label>Share IP</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
|
|
<label>Share Name</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-floating mb-2">
|
|
<input type="text" class="form-control" name="filename" placeholder="Filename" required>
|
|
<label>저장할 파일명 (예: backup.xml)</label>
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_user" placeholder="User">
|
|
<label>Share User</label>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
|
|
<label>Share Password</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-light">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
|
|
<button type="submit" class="btn btn-primary px-4">내보내기 실행</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Deploy Modal -->
|
|
<div class="modal fade" id="deployModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg">
|
|
<form action="{{ url_for('scp.import_scp') }}" method="POST">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-send-fill me-2"></i>설정 배포 (Import)</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-4">
|
|
<div class="alert alert-warning py-2 small mb-4">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i> 적용 후 서버가 재부팅될 수 있습니다.
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold small text-muted">배포 파일</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light"><i class="bi bi-file-code"></i></span>
|
|
<input type="text" class="form-control fw-bold text-primary" id="deployFilename"
|
|
name="filename" readonly>
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
|
|
<div class="form-floating mb-2">
|
|
<input type="text" class="form-control" name="target_ip" placeholder="IP" required>
|
|
<label>iDRAC IP</label>
|
|
</div>
|
|
<div class="row g-2 mb-4">
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="username" placeholder="User" required>
|
|
<label>Username</label>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="password" class="form-control" name="password" placeholder="Pwd" required>
|
|
<label>Password</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="text-success fw-bold mb-3 small text-uppercase">소스 위치 (CIFS Share)</h6>
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-8">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
|
|
<label>Share IP</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
|
|
<label>Share Name</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row g-2 mb-4">
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control" name="share_user" placeholder="User">
|
|
<label>Share User</label>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="form-floating">
|
|
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
|
|
<label>Share Password</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-floating">
|
|
<select class="form-select" name="import_mode" id="importMode">
|
|
<option value="Replace">전체 교체 (Replace)</option>
|
|
<option value="Append">변경분만 적용 (Append)</option>
|
|
</select>
|
|
<label for="importMode">적용 모드</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-light">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
|
|
<button type="submit" class="btn btn-danger px-4">배포 시작</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="{{ url_for('static', filename='js/scp.js') }}"></script>
|
|
<script>
|
|
// 드래그 앤 드롭 파일 처리
|
|
function handleFileSelect(input) {
|
|
const fileName = input.files[0]?.name;
|
|
const dropZoneText = document.getElementById('dropZoneText');
|
|
if (fileName) {
|
|
dropZoneText.innerHTML = `<span class="text-primary fw-bold">${fileName}</span><br><span class="text-muted small">파일이 선택되었습니다.</span>`;
|
|
document.getElementById('dropZone').classList.add('border-primary', 'bg-light');
|
|
} else {
|
|
dropZoneText.innerHTML = '클릭하여 파일 선택<br>또는 파일을 여기로 드래그';
|
|
document.getElementById('dropZone').classList.remove('border-primary', 'bg-light');
|
|
}
|
|
}
|
|
|
|
// 드래그 효과
|
|
const dropZone = document.getElementById('dropZone');
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
function highlight(e) {
|
|
dropZone.classList.add('dragover');
|
|
}
|
|
|
|
function unhighlight(e) {
|
|
dropZone.classList.remove('dragover');
|
|
}
|
|
|
|
dropZone.addEventListener('drop', handleDrop, false);
|
|
|
|
function handleDrop(e) {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
const input = document.getElementById('xmlFile');
|
|
|
|
if (files.length > 0) {
|
|
input.files = files; // input에 파일 할당
|
|
handleFileSelect(input);
|
|
}
|
|
}
|
|
|
|
// 툴팁 초기화 및 자동 닫기
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
``` |