Files
DevOpsLab/app/templates/pages/presets/edit.html
Сергей Антропов d4b0d6f848 Исправление синтаксической ошибки в molecule_executor.py и обновление k8s preset'ов
- Исправлена незакрытая скобка в _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
2026-02-16 00:31:09 +03:00

568 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Редактировать {{ preset.name }} - DevOpsLab{% endblock %}
{% block page_title %}Редактирование preset'а: {{ preset.name }}{% endblock %}
{% block header_actions %}
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div x-data="presetEditor()">
<form
hx-post="/api/v1/presets/{{ preset.name }}/update?category={{ preset.category }}"
hx-swap="none"
@submit.prevent="submitForm"
class="card"
>
<!-- Базовая информация -->
<div class="card-header">
<h5 class="mb-0">Базовая информация</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Имя preset'а</label>
<input
type="text"
value="{{ preset.name }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Имя preset'а нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea
name="description"
x-model="formData.description"
rows="2"
class="form-control"
placeholder="Описание preset'а..."
>{{ preset.data.description if preset.data and preset.data.description else '' }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">Категория</label>
<input
type="text"
value="{{ preset.category }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Категорию нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Docker Network</label>
<input
type="text"
name="docker_network"
x-model="formData.docker_network"
class="form-control"
placeholder="labnet"
>
</div>
</div>
<!-- Хосты -->
<div class="card-header">
<h5 class="mb-0">Хосты</h5>
</div>
<div class="card-body">
<div class="space-y-2 mb-3" x-ref="hostsContainer">
<template x-for="(host, index) in formData.hosts" :key="index">
<div class="card mb-2">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-md-2">
<label class="form-label small">Имя хоста</label>
<input
type="text"
x-model="host.name"
placeholder="u1"
class="form-control"
required
>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Семейство образа</label>
<select
x-model="host.family"
class="form-select"
>
<option value="ubuntu20">Ubuntu 20</option>
<option value="ubuntu22">Ubuntu 22</option>
<option value="ubuntu24">Ubuntu 24</option>
<option value="debian9">Debian 9</option>
<option value="debian10">Debian 10</option>
<option value="debian11">Debian 11</option>
<option value="debian12">Debian 12</option>
<option value="centos7">CentOS 7</option>
<option value="centos8">CentOS 8</option>
<option value="centos9">CentOS 9</option>
<option value="alma">AlmaLinux</option>
<option value="rocky">Rocky Linux</option>
<option value="rhel">RHEL</option>
<option value="redos">RedOS</option>
<option value="astra">Astra Linux</option>
<option value="alt9">Alt Linux 9</option>
<option value="alt10">Alt Linux 10</option>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Группы (через запятую)</label>
<input
type="text"
x-model="host.groups_str"
placeholder="test, web"
class="form-control"
@input="updateHostGroups(index)"
>
</div>
<div class="col-12 col-md-2">
<label class="form-label small">Тип (опционально)</label>
<select
x-model="host.type"
class="form-select"
>
<option value="">Обычный</option>
<option value="dind">DinD</option>
<option value="dood">DOoD</option>
</select>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeHost(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<button
type="button"
@click="addHost"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить хост
</button>
</div>
<!-- Образы -->
<div class="card-header">
<h5 class="mb-0">Docker образы</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="imagesContainer">
<template x-for="(image, key) in formData.images" :key="key">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-4">
<input
type="text"
:value="key"
class="form-control"
readonly
disabled
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="formData.images[key]"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu22-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeImage(key)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-md-4">
<input
type="text"
x-model="newImageKey"
class="form-control"
placeholder="Новый ключ (ubuntu25)"
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="newImageValue"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu25-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="addImage"
class="btn btn-outline-primary w-100"
>
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<!-- Systemd Defaults -->
<div class="card-header">
<h5 class="mb-0">Systemd Defaults</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
x-model="formData.systemd_defaults.privileged"
id="privileged"
>
<label class="form-check-label" for="privileged">
Privileged
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Command</label>
<input
type="text"
x-model="formData.systemd_defaults.command"
class="form-control"
placeholder="/sbin/init"
>
</div>
<div class="col-12">
<label class="form-label">Volumes (по одному на строку)</label>
<textarea
x-model="formData.systemd_defaults.volumes_str"
@input="updateVolumes"
rows="3"
class="form-control"
placeholder="/sys/fs/cgroup:/sys/fs/cgroup:rw"
></textarea>
</div>
<div class="col-12">
<label class="form-label">Tmpfs (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.tmpfs_str"
@input="updateTmpfs"
class="form-control"
placeholder="/run, /run/lock"
>
</div>
<div class="col-12">
<label class="form-label">Capabilities (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.capabilities_str"
@input="updateCapabilities"
class="form-control"
placeholder="SYS_ADMIN"
>
</div>
</div>
</div>
{% if preset.category == 'k8s' %}
<!-- Kind Clusters (только для k8s) -->
<div class="card-header">
<h5 class="mb-0">Kind Clusters</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="clustersContainer">
<template x-for="(cluster, index) in formData.kind_clusters" :key="index">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-10">
<input
type="text"
x-model="formData.kind_clusters[index]"
class="form-control"
placeholder="cluster-name"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeCluster(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<button
type="button"
@click="addCluster"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить кластер
</button>
</div>
{% endif %}
<!-- Скрытые поля для отправки -->
<input
type="hidden"
name="hosts"
:value="JSON.stringify(formData.hosts.map(h => ({
name: h.name,
family: h.family,
groups: h.groups,
type: h.type || undefined,
supported_platforms: h.supported_platforms || undefined
})))"
>
<input
type="hidden"
name="images"
:value="JSON.stringify(formData.images)"
>
<input
type="hidden"
name="systemd_defaults"
:value="JSON.stringify({
privileged: formData.systemd_defaults.privileged,
command: formData.systemd_defaults.command,
volumes: formData.systemd_defaults.volumes,
tmpfs: formData.systemd_defaults.tmpfs,
capabilities: formData.systemd_defaults.capabilities
})"
>
{% if preset.category == 'k8s' %}
<input
type="hidden"
name="kind_clusters"
:value="JSON.stringify(formData.kind_clusters)"
>
{% endif %}
<!-- Кнопки -->
<div class="card-footer">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить изменения
</button>
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>
Отмена
</a>
</div>
</div>
</form>
</div>
<script>
function presetEditor() {
// Парсинг данных preset'а из шаблона
const presetData = {% if preset.data %}{{ preset.data | tojson }}{% else %}{}{% endif %};
// Инициализация хостов
const hosts = presetData.hosts || [];
const formattedHosts = hosts.map(host => ({
name: host.name || '',
family: host.family || 'ubuntu22',
groups: host.groups || [],
groups_str: Array.isArray(host.groups) ? host.groups.join(', ') : (host.groups || ''),
type: host.type || '',
supported_platforms: host.supported_platforms || []
}));
// Инициализация образов
const images = presetData.images || {};
// Инициализация systemd defaults
const systemdDefaults = presetData.systemd_defaults || {};
const volumes = systemdDefaults.volumes || [];
const tmpfs = systemdDefaults.tmpfs || [];
const capabilities = systemdDefaults.capabilities || [];
// Извлечение описания из комментария в content
let description = '';
{% if preset.content %}
const contentLines = {{ preset.content | tojson }}.split('\n');
for (const line of contentLines) {
if (line.trim().startsWith('#description:')) {
description = line.split('#description:')[1].trim();
break;
}
}
{% endif %}
return {
formData: {
description: description,
docker_network: presetData.docker_network || 'labnet',
hosts: formattedHosts.length > 0 ? formattedHosts : [{
name: 'u1',
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
}],
images: images,
systemd_defaults: {
privileged: systemdDefaults.privileged !== undefined ? systemdDefaults.privileged : true,
command: systemdDefaults.command || '/sbin/init',
volumes: volumes,
volumes_str: volumes.join('\n'),
tmpfs: tmpfs,
tmpfs_str: tmpfs.join(', '),
capabilities: capabilities,
capabilities_str: capabilities.join(', ')
},
kind_clusters: presetData.kind_clusters || []
},
newImageKey: '',
newImageValue: '',
addHost() {
this.formData.hosts.push({
name: `u${this.formData.hosts.length + 1}`,
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
});
},
removeHost(index) {
this.formData.hosts.splice(index, 1);
},
updateHostGroups(index) {
const host = this.formData.hosts[index];
host.groups = host.groups_str.split(',').map(g => g.trim()).filter(g => g);
},
addImage() {
if (this.newImageKey && this.newImageValue) {
this.formData.images[this.newImageKey] = this.newImageValue;
this.newImageKey = '';
this.newImageValue = '';
}
},
removeImage(key) {
delete this.formData.images[key];
},
updateVolumes() {
this.formData.systemd_defaults.volumes =
this.formData.systemd_defaults.volumes_str
.split('\n')
.map(v => v.trim())
.filter(v => v);
},
updateTmpfs() {
this.formData.systemd_defaults.tmpfs =
this.formData.systemd_defaults.tmpfs_str
.split(',')
.map(t => t.trim())
.filter(t => t);
},
updateCapabilities() {
this.formData.systemd_defaults.capabilities =
this.formData.systemd_defaults.capabilities_str
.split(',')
.map(c => c.trim())
.filter(c => c);
},
addCluster() {
this.formData.kind_clusters.push('');
},
removeCluster(index) {
this.formData.kind_clusters.splice(index, 1);
},
submitForm(event) {
// HTMX обработает отправку
}
}
}
// Обработка ответа от HTMX для обновления пресета
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('htmx:afterRequest', function(event) {
// Проверяем, что это запрос на обновление пресета
if (event.detail.path && event.detail.path.includes('/api/v1/presets/') && event.detail.path.includes('/update')) {
if (event.detail.xhr.status === 200) {
try {
const response = JSON.parse(event.detail.xhr.responseText);
if (response.success) {
// Показываем модальное окно с успешным сообщением
if (window.showMessageModal) {
window.showMessageModal(
response.message || 'Preset успешно обновлен',
'success',
'Успешно',
function() {
// После закрытия модального окна обновляем страницу
location.reload();
}
);
} else {
// Если функция недоступна, просто обновляем страницу
location.reload();
}
}
} catch (e) {
console.error('Ошибка парсинга ответа:', e);
if (window.showMessageModal) {
window.showMessageModal('Ошибка при обновлении preset', 'error');
}
}
} else {
// Ошибка - показываем в модальном окне
try {
const response = JSON.parse(event.detail.xhr.responseText);
const errorMessage = response.detail || response.message || 'Ошибка при обновлении preset';
if (window.showMessageModal) {
window.showMessageModal(errorMessage, 'error');
} else {
alert(errorMessage);
}
} catch (e) {
if (window.showMessageModal) {
window.showMessageModal('Ошибка при обновлении preset', 'error');
} else {
alert('Ошибка при обновлении preset');
}
}
}
}
});
});
</script>
{% endblock %}