first commit
This commit is contained in:
parent
185e8dc56f
commit
8dc6f21719
|
|
@ -0,0 +1,297 @@
|
||||||
|
frappe.ui.form.on("Incus Container", {
|
||||||
|
refresh: function (frm) {
|
||||||
|
// Кнопка: Создать контейнер
|
||||||
|
if (frm.doc.status === "Creating") {
|
||||||
|
frm.add_custom_button("Создать", function () {
|
||||||
|
// Показываем индикатор загрузки
|
||||||
|
frappe.show_alert({
|
||||||
|
message: "Создание контейнера...",
|
||||||
|
indicator: "blue"
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "incus_container.incus_container.doctype.incus_container.incus_container.create_container",
|
||||||
|
args: {
|
||||||
|
name: frm.doc.container_name,
|
||||||
|
image_alias: frm.doc.image || "alpine/edge",
|
||||||
|
container_type: frm.doc.container_type || "container",
|
||||||
|
wait_completion: true
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
const response = r.message;
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Успех",
|
||||||
|
indicator: "green",
|
||||||
|
message: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показываем детали операции если есть
|
||||||
|
if (response.operation_data) {
|
||||||
|
console.log("Operation details:", response.operation_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статус документа
|
||||||
|
frm.set_value('status', 'Created');
|
||||||
|
frm.save();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Ошибка создания контейнера",
|
||||||
|
indicator: "red",
|
||||||
|
message: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error("Error details:", response.error);
|
||||||
|
|
||||||
|
// При ошибке ставим статус Error
|
||||||
|
frm.set_value('status', 'Error');
|
||||||
|
frm.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(r) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Системная ошибка",
|
||||||
|
indicator: "red",
|
||||||
|
message: "Произошла ошибка при вызове API"
|
||||||
|
});
|
||||||
|
console.error("API call error:", r);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, "Управление");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка: Быстрое создание (без ожидания)
|
||||||
|
if (frm.doc.status === "Creating") {
|
||||||
|
frm.add_custom_button("Создать (быстро)", function () {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: "Отправка запроса на создание контейнера...",
|
||||||
|
indicator: "orange"
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "incus_container.incus_container.doctype.incus_container.incus_container.create_container",
|
||||||
|
args: {
|
||||||
|
name: frm.doc.container_name,
|
||||||
|
image_alias: frm.doc.image || "alpine/edge",
|
||||||
|
container_type: frm.doc.container_type || "container",
|
||||||
|
wait_completion: false
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
const response = r.message;
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Запрос отправлен",
|
||||||
|
indicator: "blue",
|
||||||
|
message: `${response.message}<br><small>Операция: ${response.operation_url}</small>`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Operation URL:", response.operation_url);
|
||||||
|
|
||||||
|
// Обновляем статус на "В процессе"
|
||||||
|
frm.set_value('status', 'Processing');
|
||||||
|
frm.save();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Ошибка",
|
||||||
|
indicator: "red",
|
||||||
|
message: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error("Error details:", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, "Управление");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка: Создать с пользовательскими параметрами
|
||||||
|
frm.add_custom_button("Создать с параметрами", function () {
|
||||||
|
// Создаем диалог для ввода параметров
|
||||||
|
let dialog = new frappe.ui.Dialog({
|
||||||
|
title: 'Создание контейнера',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Имя контейнера',
|
||||||
|
fieldname: 'container_name',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
reqd: 1,
|
||||||
|
default: frm.doc.container_name || ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Образ (алиас)',
|
||||||
|
fieldname: 'image_alias',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
reqd: 1,
|
||||||
|
default: frm.doc.image || 'alpine/edge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Тип контейнера',
|
||||||
|
fieldname: 'container_type',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options: 'container\nvirtual-machine',
|
||||||
|
default: frm.doc.container_type || 'container'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ждать завершения',
|
||||||
|
fieldname: 'wait_completion',
|
||||||
|
fieldtype: 'Check',
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action_label: 'Создать',
|
||||||
|
primary_action(values) {
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
frappe.show_alert({
|
||||||
|
message: "Создание контейнера...",
|
||||||
|
indicator: "blue"
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "incus_container.incus_container.doctype.incus_container.incus_container.create_container",
|
||||||
|
args: {
|
||||||
|
name: values.container_name,
|
||||||
|
image_alias: values.image_alias,
|
||||||
|
container_type: values.container_type,
|
||||||
|
wait_completion: values.wait_completion
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
const response = r.message;
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Успех",
|
||||||
|
indicator: "green",
|
||||||
|
message: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем поля документа если создавался для него
|
||||||
|
if (values.container_name === frm.doc.container_name) {
|
||||||
|
frm.set_value('image', values.image_alias);
|
||||||
|
frm.set_value('container_type', values.container_type);
|
||||||
|
frm.set_value('status', values.wait_completion ? 'Created' : 'Processing');
|
||||||
|
frm.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Ошибка",
|
||||||
|
indicator: "red",
|
||||||
|
message: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error("Error details:", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
}, "Управление");
|
||||||
|
|
||||||
|
// Кнопка: Показать список контейнеров
|
||||||
|
frm.add_custom_button("Список контейнеров", function () {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: "Загружаем список контейнеров...",
|
||||||
|
indicator: "blue"
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "incus_container.incus_container.doctype.incus_container.incus_container.list_containers",
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
const response = r.message;
|
||||||
|
|
||||||
|
if (response.success && response.containers) {
|
||||||
|
// Создаем HTML таблицу для отображения
|
||||||
|
let table_html = `
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Архитектура</th>
|
||||||
|
<th>Создан</th>
|
||||||
|
<th>Последнее использование</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
response.containers.forEach(container => {
|
||||||
|
// Определяем цвет статуса
|
||||||
|
let status_color = 'text-muted';
|
||||||
|
if (container.status === 'Running') status_color = 'text-success';
|
||||||
|
else if (container.status === 'Stopped') status_color = 'text-warning';
|
||||||
|
else if (container.status === 'Error') status_color = 'text-danger';
|
||||||
|
|
||||||
|
// Форматируем даты
|
||||||
|
let created_at = container.created_at !== 'Unknown' ?
|
||||||
|
new Date(container.created_at).toLocaleString() : 'Unknown';
|
||||||
|
let last_used_at = container.last_used_at !== 'Unknown' ?
|
||||||
|
new Date(container.last_used_at).toLocaleString() : 'Unknown';
|
||||||
|
|
||||||
|
table_html += `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${container.name}</strong></td>
|
||||||
|
<td><span class="${status_color}">${container.status}</span></td>
|
||||||
|
<td>${container.type}</td>
|
||||||
|
<td>${container.architecture}</td>
|
||||||
|
<td>${created_at}</td>
|
||||||
|
<td>${last_used_at}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
table_html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p><strong>Всего контейнеров: ${response.total_count}</strong></p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Показываем диалог с таблицей
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Список контейнеров Incus",
|
||||||
|
message: table_html,
|
||||||
|
wide: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Также выводим в консоль для разработчика
|
||||||
|
console.log("Containers list:", response.containers);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Ошибка",
|
||||||
|
indicator: "red",
|
||||||
|
message: response.message || "Не удалось получить список контейнеров"
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error("Error:", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(r) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: "Системная ошибка",
|
||||||
|
indicator: "red",
|
||||||
|
message: "Произошла ошибка при получении списка контейнеров"
|
||||||
|
});
|
||||||
|
console.error("API call error:", r);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, "Действия");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-09-03 19:23:42.889432",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"container_name",
|
||||||
|
"image",
|
||||||
|
"container_type",
|
||||||
|
"server",
|
||||||
|
"status",
|
||||||
|
"ip_address",
|
||||||
|
"section_break_details",
|
||||||
|
"created_at",
|
||||||
|
"last_used_at",
|
||||||
|
"architecture",
|
||||||
|
"section_break_pmeb"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "container_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Container Name",
|
||||||
|
"reqd": 1,
|
||||||
|
"description": "Уникальное имя контейнера в Incus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Image Alias",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": "alpine/edge",
|
||||||
|
"description": "Алиас образа Incus (например: alpine/edge, ubuntu/22.04)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "container_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Container Type",
|
||||||
|
"options": "container\nvirtual-machine",
|
||||||
|
"default": "container",
|
||||||
|
"description": "Тип экземпляра: контейнер или виртуальная машина"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "server",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Server",
|
||||||
|
"default": "router1",
|
||||||
|
"description": "Адрес Incus API (пример: router1 или 10.0.0.1)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Creating\nProcessing\nCreated\nRunning\nStopped\nError",
|
||||||
|
"default": "Creating",
|
||||||
|
"read_only": 1,
|
||||||
|
"description": "Текущий статус контейнера"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "ip_address",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "IP Address",
|
||||||
|
"read_only": 1,
|
||||||
|
"description": "IP адрес контейнера (если назначен)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_details",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Детали контейнера"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "created_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Created At",
|
||||||
|
"read_only": 1,
|
||||||
|
"description": "Дата и время создания контейнера в Incus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "last_used_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Last Used At",
|
||||||
|
"read_only": 1,
|
||||||
|
"description": "Дата и время последнего использования"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "architecture",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Architecture",
|
||||||
|
"read_only": 1,
|
||||||
|
"description": "Архитектура контейнера (x86_64, aarch64, etc.)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_pmeb",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-09-03 19:23:42.889432",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Incus Manager",
|
||||||
|
"name": "Incus Container",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
import frappe
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
# === Настройки Incus API ===
|
||||||
|
INCUS_HOST = "https://10.220.187.117:8443"
|
||||||
|
CLIENT_CERT = ("/home/frappe/frappe-bench/apps/jey_erp/client.crt", "/home/frappe/frappe-bench/apps/jey_erp/client.key")
|
||||||
|
VERIFY = False
|
||||||
|
|
||||||
|
class IncusContainer(Document):
|
||||||
|
"""DocType для управления контейнерами Incus"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait_for_operation(operation_url, timeout=60):
|
||||||
|
"""
|
||||||
|
Ждёт завершения асинхронной операции в Incus
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
r = requests.get(
|
||||||
|
operation_url,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
cert=CLIENT_CERT,
|
||||||
|
verify=VERIFY
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
op_data = r.json()
|
||||||
|
metadata = op_data.get("metadata", {})
|
||||||
|
status = metadata.get("status")
|
||||||
|
|
||||||
|
if status == "Success":
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"operation_data": op_data,
|
||||||
|
"message": "Операция успешно завершена"
|
||||||
|
}
|
||||||
|
elif status == "Failure":
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"operation_data": op_data,
|
||||||
|
"error": metadata.get("err", "Unknown error"),
|
||||||
|
"message": "Операция завершена с ошибкой"
|
||||||
|
}
|
||||||
|
elif status in ["Running", "Pending"]:
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"message": "Ошибка при проверке статуса операции"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Timeout",
|
||||||
|
"message": f"Операция не завершилась за {timeout} секунд"
|
||||||
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def list_containers():
|
||||||
|
"""
|
||||||
|
Получает список контейнеров
|
||||||
|
"""
|
||||||
|
url = f"{INCUS_HOST}/1.0/instances"
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(
|
||||||
|
url,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
cert=CLIENT_CERT,
|
||||||
|
verify=VERIFY
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
containers_data = r.json()
|
||||||
|
|
||||||
|
# Получаем детальную информацию о каждом контейнере
|
||||||
|
detailed_containers = []
|
||||||
|
|
||||||
|
if containers_data.get("metadata"):
|
||||||
|
for container_url in containers_data["metadata"]:
|
||||||
|
# container_url выглядит как "/1.0/instances/container_name"
|
||||||
|
container_name = container_url.split("/")[-1]
|
||||||
|
|
||||||
|
# Получаем подробную информацию о контейнере
|
||||||
|
detail_url = f"{INCUS_HOST}{container_url}"
|
||||||
|
try:
|
||||||
|
detail_r = requests.get(
|
||||||
|
detail_url,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
cert=CLIENT_CERT,
|
||||||
|
verify=VERIFY
|
||||||
|
)
|
||||||
|
detail_r.raise_for_status()
|
||||||
|
container_detail = detail_r.json()
|
||||||
|
|
||||||
|
if container_detail.get("metadata"):
|
||||||
|
metadata = container_detail["metadata"]
|
||||||
|
detailed_containers.append({
|
||||||
|
"name": container_name,
|
||||||
|
"status": metadata.get("status", "Unknown"),
|
||||||
|
"type": metadata.get("type", "Unknown"),
|
||||||
|
"architecture": metadata.get("architecture", "Unknown"),
|
||||||
|
"created_at": metadata.get("created_at", "Unknown"),
|
||||||
|
"last_used_at": metadata.get("last_used_at", "Unknown")
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
# Если не удалось получить детали, добавляем базовую информацию
|
||||||
|
detailed_containers.append({
|
||||||
|
"name": container_name,
|
||||||
|
"status": "Unknown",
|
||||||
|
"type": "Unknown",
|
||||||
|
"architecture": "Unknown",
|
||||||
|
"created_at": "Unknown",
|
||||||
|
"last_used_at": "Unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"containers": detailed_containers,
|
||||||
|
"total_count": len(detailed_containers),
|
||||||
|
"message": f"Найдено контейнеров: {len(detailed_containers)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"message": "Ошибка получения списка контейнеров"
|
||||||
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_container(name, image_alias="alpine/edge", container_type="container", wait_completion=True):
|
||||||
|
"""
|
||||||
|
Создаёт новый контейнер
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Имя контейнера
|
||||||
|
image_alias (str): Алиас образа (по умолчанию alpine/edge)
|
||||||
|
container_type (str): Тип контейнера (по умолчанию container)
|
||||||
|
wait_completion (bool): Ждать ли завершения операции (по умолчанию True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат операции создания контейнера
|
||||||
|
"""
|
||||||
|
url = f"{INCUS_HOST}/1.0/instances"
|
||||||
|
|
||||||
|
# Подготавливаем данные для создания контейнера
|
||||||
|
container_data = {
|
||||||
|
"name": name,
|
||||||
|
"type": container_type,
|
||||||
|
"source": {
|
||||||
|
"type": "image",
|
||||||
|
"alias": image_alias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Отправляем запрос на создание контейнера
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
cert=CLIENT_CERT,
|
||||||
|
verify=VERIFY,
|
||||||
|
data=json.dumps(container_data)
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
response_data = r.json()
|
||||||
|
|
||||||
|
# Если wait_completion=False, возвращаем сразу
|
||||||
|
if not wait_completion:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"container_name": name,
|
||||||
|
"operation_url": response_data.get("operation"),
|
||||||
|
"message": f"Запрос на создание контейнера '{name}' отправлен"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ждём завершения операции
|
||||||
|
operation_url = response_data.get("operation")
|
||||||
|
if operation_url:
|
||||||
|
full_operation_url = f"{INCUS_HOST}{operation_url}"
|
||||||
|
operation_result = wait_for_operation(full_operation_url, timeout=120)
|
||||||
|
|
||||||
|
if operation_result["success"]:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"container_name": name,
|
||||||
|
"image_alias": image_alias,
|
||||||
|
"container_type": container_type,
|
||||||
|
"operation_data": operation_result["operation_data"],
|
||||||
|
"message": f"Контейнер '{name}' успешно создан"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": name,
|
||||||
|
"error": operation_result.get("error"),
|
||||||
|
"operation_data": operation_result.get("operation_data"),
|
||||||
|
"message": f"Ошибка при создании контейнера '{name}': {operation_result.get('error')}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": name,
|
||||||
|
"error": "No operation URL returned",
|
||||||
|
"message": f"Не удалось получить URL операции для контейнера '{name}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
# Обрабатываем HTTP ошибки более детально
|
||||||
|
error_detail = ""
|
||||||
|
try:
|
||||||
|
error_response = e.response.json()
|
||||||
|
error_detail = error_response.get("error", str(e))
|
||||||
|
except:
|
||||||
|
error_detail = str(e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": name,
|
||||||
|
"error": error_detail,
|
||||||
|
"status_code": e.response.status_code if e.response else None,
|
||||||
|
"message": f"HTTP ошибка при создании контейнера '{name}': {error_detail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": name,
|
||||||
|
"error": str(e),
|
||||||
|
"message": f"Ошибка сети при создании контейнера '{name}': {str(e)}"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": name,
|
||||||
|
"error": str(e),
|
||||||
|
"message": f"Неожиданная ошибка при создании контейнера '{name}': {str(e)}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright (c) 2025, Jey ERP and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record depdendencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestIncusContainer(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for IncusContainer.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestIncusContainer(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for IncusContainer.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
Loading…
Reference in New Issue