Files
DevOpsLab/app/templates/pages/presets/create.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

464 lines
19 KiB
HTML
Raw 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 - DevOpsLab{% endblock %}
{% block page_title %}Создание нового preset'а{% endblock %}
{% block header_actions %}
<a href="/presets" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад к списку
</a>
{% endblock %}
{% block content %}
<div x-data="presetCreator()" x-init="init()">
<form
hx-post="/api/v1/presets/create"
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"
name="preset_name"
x-model="formData.preset_name"
required
pattern="[a-z0-9_-]+"
class="form-control"
placeholder="my-preset"
>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea
name="description"
x-model="formData.description"
rows="2"
class="form-control"
placeholder="Описание preset'а..."
></textarea>
</div>
<div class="mb-3">
<label class="form-label">Категория</label>
<select
name="category"
x-model="formData.category"
class="form-select"
>
<option value="main">Основные</option>
<option value="k8s">Kubernetes</option>
</select>
</div>
</div>
<!-- Docker образы -->
<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 align-items-end">
<div class="col-12 col-md-4">
<label class="form-label small">Dockerfile</label>
<select
x-model="selectedDockerfile"
@change="onDockerfileSelected"
class="form-select"
:disabled="!dockerfilesLoaded"
>
<option value="">Выберите Dockerfile...</option>
<template x-for="dockerfile in dockerfiles" :key="dockerfile.id">
<option :value="dockerfile.name" x-text="dockerfile.name"></option>
</template>
</select>
<div class="form-text" x-show="!dockerfilesLoaded">
<i class="fas fa-spinner fa-spin me-1"></i>Загрузка Dockerfiles...
</div>
<div class="form-text text-danger" x-show="dockerfilesLoaded && dockerfiles.length === 0">
<i class="fas fa-exclamation-triangle me-1"></i>Dockerfiles не найдены
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label small">Значение образа</label>
<input
type="text"
x-model="newImageValue"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu25"
>
</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>
<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-3">
<label class="form-label small">Имя хоста</label>
<input
type="text"
x-model="host.name"
placeholder="u1"
class="form-control"
>
</div>
<div class="col-12 col-md-4">
<label class="form-label small">Семейство образа</label>
<select
x-model="host.family"
class="form-select"
>
<option value="">Выберите образ...</option>
<template x-for="(image, key) in formData.images" :key="key">
<option :value="key" x-text="key"></option>
</template>
</select>
</div>
<div class="col-12 col-md-4">
<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-1">
<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>
<!-- 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"
>/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>
<!-- Скрытые поля -->
<input
type="hidden"
name="hosts"
:value="JSON.stringify(formData.hosts.map(h => ({name: h.name, family: h.family, groups: h.groups})))"
>
<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
})"
>
<!-- Кнопки -->
<div class="card-footer">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-check me-2"></i>
Создать preset
</button>
<a href="/presets" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>
Отмена
</a>
</div>
</div>
</form>
</div>
<script>
function presetCreator() {
return {
formData: {
preset_name: '',
description: '',
category: 'main',
hosts: [{
name: 'u1',
family: '',
groups_str: 'test, web',
groups: ['test', 'web']
}],
images: {},
systemd_defaults: {
privileged: true,
command: '/sbin/init',
volumes_str: '/sys/fs/cgroup:/sys/fs/cgroup:rw',
volumes: ['/sys/fs/cgroup:/sys/fs/cgroup:rw'],
tmpfs_str: '/run, /run/lock',
tmpfs: ['/run', '/run/lock'],
capabilities_str: 'SYS_ADMIN',
capabilities: ['SYS_ADMIN']
}
},
dockerfiles: {{ dockerfiles | tojson }},
dockerfilesLoaded: true,
selectedDockerfile: '',
newImageKey: '',
newImageValue: '',
init() {
// Dockerfiles уже загружены из шаблона
console.log('Загружено Dockerfiles из шаблона:', this.dockerfiles.length);
},
onDockerfileSelected() {
if (this.selectedDockerfile) {
// Устанавливаем ключ образа из выбранного Dockerfile
this.newImageKey = this.selectedDockerfile;
// Автоматически подставляем значение образа в формате inecs/ansible-lab:{name}
this.newImageValue = `inecs/ansible-lab:${this.selectedDockerfile}`;
}
},
addHost() {
this.formData.hosts.push({
name: `u${this.formData.hosts.length + 1}`,
family: '',
groups_str: 'test',
groups: ['test']
});
},
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 = '';
this.selectedDockerfile = '';
}
},
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);
},
submitForm(event) {
// Обновляем массивы перед отправкой
this.updateVolumes();
this.updateTmpfs();
this.updateCapabilities();
// HTMX обработает отправку
}
}
}
// Обработка ответа от HTMX для создания пресета
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('htmx:afterRequest', function(event) {
// Проверяем, что это запрос на создание пресета
if (event.detail.path === '/api/v1/presets/create') {
if (event.detail.xhr.status === 201 || 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 '${response.preset_name}' успешно создан`,
'success',
'Успешно',
function() {
// После закрытия модального окна перенаправляем на страницу списка пресетов
window.location.href = '/presets';
}
);
} else {
// Если функция недоступна, просто перенаправляем
window.location.href = '/presets';
}
}
} 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 %}