- Исправлена незакрытая скобка в _build_test_command (строка 745) - Добавлена поддержка k8s preset'ов: выполнение create_k8s_cluster.py перед create.yml - Обновлены образы в k8s preset'ах: заменен недоступный ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy на inecs/ansible-lab:ubuntu22-latest - Обновлены preset'ы в базе данных через SQL - Обновлены файлы: k8s-single.yml, k8s-multi.yml, k8s-istio-full.yml
447 lines
15 KiB
HTML
447 lines
15 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ preset.name }} - DevOpsLab{% endblock %}
|
||
{% block page_title %}Preset: {{ preset.name }}{% endblock %}
|
||
|
||
{% block header_actions %}
|
||
<button
|
||
type="button"
|
||
class="btn btn-success btn-sm"
|
||
onclick="openPresetTestModal()"
|
||
title="Запустить тест preset'а"
|
||
data-bs-toggle="modal"
|
||
data-bs-target="#presetTestModal"
|
||
>
|
||
<i class="fas fa-play me-2"></i>
|
||
Запустить
|
||
</button>
|
||
<a href="/presets/{{ preset.name }}/edit?category={{ preset.category }}" class="btn btn-primary btn-sm">
|
||
<i class="fas fa-edit me-2"></i>
|
||
Редактировать
|
||
</a>
|
||
<a href="/presets" class="btn btn-secondary btn-sm">
|
||
<i class="fas fa-arrow-left me-2"></i>
|
||
Назад
|
||
</a>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<!-- Информация о preset'е -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Информация о preset'е</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<strong>Имя:</strong> {{ preset.name }}<br>
|
||
<strong>Категория:</strong>
|
||
<span class="badge bg-{% if preset.category == 'k8s' %}info{% else %}primary{% endif %}">
|
||
{{ preset.category }}
|
||
</span>
|
||
</div>
|
||
<div class="col-md-6">
|
||
{% if preset.data %}
|
||
<strong>Docker сеть:</strong> {{ preset.data.get('docker_network', 'labnet') }}<br>
|
||
<strong>Хостов:</strong> {{ preset.data.get('hosts', [])|length }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if preset.data and preset.data.get('description') %}
|
||
<div class="mb-3">
|
||
<strong>Описание:</strong>
|
||
<p class="text-muted mb-0">{{ preset.data.description }}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if preset.data and preset.data.get('hosts') %}
|
||
<div class="mb-3">
|
||
<strong>Хосты:</strong>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered w-100">
|
||
<thead>
|
||
<tr>
|
||
<th>Имя</th>
|
||
<th>Семейство</th>
|
||
<th>Группы</th>
|
||
<th>Тип</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for host in preset.data.hosts %}
|
||
<tr>
|
||
<td>{{ host.name }}</td>
|
||
<td>
|
||
<span class="badge bg-secondary">{{ host.family }}</span>
|
||
</td>
|
||
<td>
|
||
{% if host.groups %}
|
||
{% for group in host.groups %}
|
||
<span class="badge bg-info me-1">{{ group }}</span>
|
||
{% endfor %}
|
||
{% else %}
|
||
<span class="text-muted">—</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ host.type or '—' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if preset.data and preset.data.get('images') %}
|
||
<div class="mb-3">
|
||
<strong>Docker образы:</strong>
|
||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||
{% for key, value in preset.data.images.items() %}
|
||
<span class="badge bg-success">
|
||
<i class="fas fa-cube me-1"></i>
|
||
{{ key }}: {{ value.split(':')[-1] if ':' in value else value }}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- YAML содержимое -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">YAML содержимое</h5>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<pre class="mb-0" style="background: #1e1e1e; color: #d4d4d4; padding: 1rem; margin: 0; overflow-x: auto;"><code>{{ preset.content }}</code></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Модальное окно для тестирования preset'а -->
|
||
<div class="modal fade" id="presetTestModal" tabindex="-1" aria-labelledby="presetTestModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-fullscreen">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="presetTestModalLabel">
|
||
<i class="fas fa-vial me-2"></i>
|
||
Тестирование preset'а: {{ preset.name }}
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть" onclick="stopPresetTest()"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="log-container" id="preset-test-logs">
|
||
<div class="text-center text-muted py-5">
|
||
<i class="fas fa-spinner fa-spin fa-2x mb-3"></i>
|
||
<p>Подключение к серверу...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button
|
||
type="button"
|
||
class="btn btn-warning"
|
||
onclick="stopPresetTest()"
|
||
id="modal-stop-btn"
|
||
title="Остановить тест"
|
||
>
|
||
<i class="fas fa-stop me-2"></i>
|
||
Остановить
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-info"
|
||
onclick="restartPresetTest()"
|
||
id="modal-restart-btn"
|
||
title="Перезапустить тест"
|
||
>
|
||
<i class="fas fa-redo me-2"></i>
|
||
Перезапустить
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="clearPresetTestLogs()"
|
||
title="Очистить логи"
|
||
>
|
||
<i class="fas fa-trash me-2"></i>
|
||
Очистить
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
data-bs-dismiss="modal"
|
||
onclick="stopPresetTest()"
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let testWebSocket = null;
|
||
let testRunning = false;
|
||
let presetTestModal = null;
|
||
|
||
// Инициализация модального окна
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
presetTestModal = new bootstrap.Modal(document.getElementById('presetTestModal'));
|
||
|
||
// Обработка закрытия модального окна
|
||
document.getElementById('presetTestModal').addEventListener('hidden.bs.modal', function() {
|
||
stopPresetTest();
|
||
});
|
||
});
|
||
|
||
function openPresetTestModal() {
|
||
if (testRunning) {
|
||
alert('Тест уже запущен');
|
||
return;
|
||
}
|
||
|
||
// Очищаем логи
|
||
const logsContainer = document.getElementById('preset-test-logs');
|
||
logsContainer.innerHTML = '<div class="text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x mb-3"></i><p>Подключение к серверу...</p></div>';
|
||
|
||
// Показываем модальное окно
|
||
presetTestModal.show();
|
||
|
||
// Запускаем тест
|
||
setTimeout(() => {
|
||
startPresetTest();
|
||
}, 300);
|
||
}
|
||
|
||
function startPresetTest() {
|
||
if (testRunning) {
|
||
return;
|
||
}
|
||
|
||
const logsContainer = document.getElementById('preset-test-logs');
|
||
logsContainer.innerHTML = '';
|
||
|
||
// Показываем кнопки управления
|
||
document.getElementById('modal-stop-btn').disabled = false;
|
||
document.getElementById('modal-restart-btn').disabled = false;
|
||
|
||
testRunning = true;
|
||
|
||
// Создаем WebSocket подключение
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${protocol}//${window.location.host}/ws/preset/test/{{ preset.name }}?category={{ preset.category }}`;
|
||
const ws = new WebSocket(wsUrl);
|
||
testWebSocket = ws;
|
||
|
||
ws.onopen = () => {
|
||
addLog('info', '🔌 Подключено к серверу');
|
||
ws.send(JSON.stringify({
|
||
action: 'start',
|
||
preset_name: '{{ preset.name }}',
|
||
preset_category: '{{ preset.category }}'
|
||
}));
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.type === 'log' || data.type === 'info') {
|
||
addLog(data.level || 'info', data.data);
|
||
} else if (data.type === 'error') {
|
||
addLog('error', data.data);
|
||
} else if (data.type === 'complete') {
|
||
addLog('info', data.data || '✅ Тестирование завершено');
|
||
testRunning = false;
|
||
document.getElementById('modal-stop-btn').disabled = true;
|
||
document.getElementById('modal-restart-btn').disabled = false;
|
||
ws.close();
|
||
}
|
||
} catch (e) {
|
||
console.error('Ошибка парсинга сообщения:', e);
|
||
}
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
addLog('error', `❌ Ошибка подключения WebSocket`);
|
||
testRunning = false;
|
||
document.getElementById('modal-stop-btn').disabled = true;
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
testRunning = false;
|
||
if (testWebSocket === ws) {
|
||
testWebSocket = null;
|
||
}
|
||
};
|
||
}
|
||
|
||
function stopPresetTest() {
|
||
if (testWebSocket && testWebSocket.readyState === WebSocket.OPEN) {
|
||
testWebSocket.send(JSON.stringify({ action: 'stop' }));
|
||
addLog('info', '⏹️ Остановка тестирования...');
|
||
}
|
||
if (testWebSocket) {
|
||
testWebSocket.close();
|
||
testWebSocket = null;
|
||
}
|
||
testRunning = false;
|
||
document.getElementById('modal-stop-btn').disabled = true;
|
||
document.getElementById('modal-restart-btn').disabled = false;
|
||
}
|
||
|
||
function restartPresetTest() {
|
||
stopPresetTest();
|
||
const logsContainer = document.getElementById('preset-test-logs');
|
||
logsContainer.innerHTML = '';
|
||
setTimeout(() => {
|
||
startPresetTest();
|
||
}, 500);
|
||
}
|
||
|
||
function clearPresetTestLogs() {
|
||
const logsContainer = document.getElementById('preset-test-logs');
|
||
logsContainer.innerHTML = '';
|
||
}
|
||
|
||
function addLog(level, message) {
|
||
const logsContainer = document.getElementById('preset-test-logs');
|
||
const logLine = document.createElement('div');
|
||
logLine.className = `log-line log-${level}`;
|
||
|
||
// Цвета для разных уровней логов
|
||
const colors = {
|
||
'error': '#f48771',
|
||
'warning': '#d19a66',
|
||
'info': '#61afef',
|
||
'success': '#98c379',
|
||
'debug': '#abb2bf'
|
||
};
|
||
|
||
logLine.style.color = colors[level] || colors['debug'];
|
||
logLine.textContent = message;
|
||
logsContainer.appendChild(logLine);
|
||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* Стили для модального окна тестирования preset'а - как у модального окна логов сборки */
|
||
#presetTestModal .modal-dialog {
|
||
margin: 0 !important;
|
||
max-width: 100% !important;
|
||
width: 100% !important;
|
||
height: 100vh !important;
|
||
max-height: 100vh !important;
|
||
}
|
||
|
||
#presetTestModal .modal-content {
|
||
height: 100vh !important;
|
||
max-height: 100vh !important;
|
||
margin: 0 !important;
|
||
border: none !important;
|
||
border-radius: 0 !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
}
|
||
|
||
#presetTestModal .modal-header {
|
||
flex-shrink: 0 !important;
|
||
padding: 1rem !important;
|
||
height: auto !important;
|
||
}
|
||
|
||
#presetTestModal .modal-body {
|
||
flex: 1 1 0 !important;
|
||
overflow: hidden !important;
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
width: 100% !important;
|
||
min-height: 0 !important;
|
||
max-height: none !important;
|
||
height: auto !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
position: relative !important;
|
||
}
|
||
|
||
#preset-test-logs {
|
||
position: absolute !important;
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
right: 0 !important;
|
||
bottom: 0 !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
min-height: 0 !important;
|
||
max-height: none !important;
|
||
box-sizing: border-box !important;
|
||
overflow: auto !important;
|
||
background: #1e1e1e !important;
|
||
color: #d4d4d4 !important;
|
||
padding: 1rem !important;
|
||
font-family: 'Courier New', monospace !important;
|
||
font-size: 0.875rem !important;
|
||
white-space: pre !important;
|
||
word-wrap: normal !important;
|
||
overflow-wrap: normal !important;
|
||
}
|
||
|
||
#presetTestModal .modal-footer {
|
||
flex-shrink: 0 !important;
|
||
padding: 1rem !important;
|
||
height: auto !important;
|
||
}
|
||
|
||
/* Дополнительные стили для гарантии полной высоты */
|
||
#presetTestModal.show .modal-body,
|
||
#presetTestModal.showing .modal-body {
|
||
height: calc(100vh - 180px) !important;
|
||
min-height: calc(100vh - 180px) !important;
|
||
max-height: calc(100vh - 180px) !important;
|
||
}
|
||
|
||
#presetTestModal.show #preset-test-logs,
|
||
#presetTestModal.showing #preset-test-logs {
|
||
height: calc(100vh - 180px) !important;
|
||
min-height: calc(100vh - 180px) !important;
|
||
max-height: calc(100vh - 180px) !important;
|
||
}
|
||
|
||
/* Стили для строк логов */
|
||
#preset-test-logs .log-line {
|
||
margin-bottom: 0.25rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
#preset-test-logs .log-error {
|
||
color: #f48771 !important;
|
||
}
|
||
|
||
#preset-test-logs .log-warning {
|
||
color: #d19a66 !important;
|
||
}
|
||
|
||
#preset-test-logs .log-info {
|
||
color: #61afef !important;
|
||
}
|
||
|
||
#preset-test-logs .log-success {
|
||
color: #98c379 !important;
|
||
}
|
||
|
||
#preset-test-logs .log-debug {
|
||
color: #abb2bf !important;
|
||
}
|
||
</style>
|
||
{% endblock %}
|