- Исправлена незакрытая скобка в _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
464 lines
19 KiB
HTML
464 lines
19 KiB
HTML
{% 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 %}
|