fix: adapt autotest to updated service APIs
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
|||||||
|
|
||||||
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
||||||
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
||||||
VERSION = "1.2.47"
|
VERSION = "1.2.48"
|
||||||
|
|
||||||
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
||||||
25 vcexternalip # Публичные IP адреса
|
25 vcexternalip # Публичные IP адреса
|
||||||
26 vapp # Виртуальный каталог ВМ (vApp)
|
26 vapp # Виртуальный каталог ВМ (vApp)
|
||||||
27 vc_vm_v2 # VM в Cloud Director (vc_vmV2)
|
# 27 vc_vm_v2 # отсутствует в текущем Polygon DEV
|
||||||
28 vc_vm_v3 # Виртуальная машина
|
28 vc_vm_v3 # Виртуальная машина
|
||||||
29 vcVdcGroup # Группа датацентров
|
29 vcVdcGroup # Группа датацентров
|
||||||
# 32 vmpostgre # vc_vm_postgresql_std
|
# 32 vmpostgre # vc_vm_postgresql_std
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
||||||
25 vcexternalip # Публичные IP адреса
|
25 vcexternalip # Публичные IP адреса
|
||||||
26 vapp # Виртуальный каталог ВМ (vApp)
|
26 vapp # Виртуальный каталог ВМ (vApp)
|
||||||
27 vc_vm_v2 # VM в Cloud Director (vc_vmV2)
|
# 27 vc_vm_v2 # отсутствует в текущем Polygon TEST
|
||||||
28 vc_vm_v3 # Виртуальная машина
|
28 vc_vm_v3 # Виртуальная машина
|
||||||
29 vcVdcGroup # Группа датацентров
|
29 vcVdcGroup # Группа датацентров
|
||||||
# 32 vmpostgre # vc_vm_postgresql_std
|
# 32 vmpostgre # vc_vm_postgresql_std
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import json
|
|||||||
import fcntl
|
import fcntl
|
||||||
|
|
||||||
from api.http_client import HttpClient, detect_endpoint, stand_name
|
from api.http_client import HttpClient, detect_endpoint, stand_name
|
||||||
from api.auth import get_token, get_client, get_client_id, get_stand, get_token_info
|
from api.auth import get_token, get_client, get_client_id, get_stand, get_token_info, get_mode
|
||||||
from api.utils import find_uid, uid_from_location
|
from api.utils import find_uid, uid_from_location
|
||||||
from operations.get_services import get_services, get_service_detail
|
from operations.get_services import get_services, get_service_detail
|
||||||
from operations.get_instances import get_instances
|
from operations.get_instances import get_instances
|
||||||
@@ -217,7 +217,7 @@ def api_test():
|
|||||||
if not instance_uid:
|
if not instance_uid:
|
||||||
return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400
|
return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400
|
||||||
|
|
||||||
if op_name == "delete":
|
if op_name == "delete" and get_mode() == "cloud":
|
||||||
# Жёсткое удаление через CMDB (без авторизации) — для недосозданных инстансов
|
# Жёсткое удаление через CMDB (без авторизации) — для недосозданных инстансов
|
||||||
cmdb_url = f"https://cmdb-api.deck.nubes.ru/instances/{instance_uid}"
|
cmdb_url = f"https://cmdb-api.deck.nubes.ru/instances/{instance_uid}"
|
||||||
cmdb_ok, cmdb_code = client.raw_delete(cmdb_url)
|
cmdb_ok, cmdb_code = client.raw_delete(cmdb_url)
|
||||||
|
|||||||
+2
-2
@@ -223,11 +223,11 @@ def api_operations(svc_id):
|
|||||||
# Сортировка: по имени сервиса, затем по дате создания (новые сверху)
|
# Сортировка: по имени сервиса, затем по дате создания (новые сверху)
|
||||||
svc_instances.sort(key=lambda i: (
|
svc_instances.sort(key=lambda i: (
|
||||||
i.get("svc", ""),
|
i.get("svc", ""),
|
||||||
i.get("instanceConfigDtCreated") or ""
|
i.get("instanceConfigDtCreated") or i.get("dtCreate") or ""
|
||||||
), reverse=True)
|
), reverse=True)
|
||||||
# reverse=True даёт обратный порядок по обоим ключам.
|
# reverse=True даёт обратный порядок по обоим ключам.
|
||||||
# Чтобы сервисы шли A→Я а даты новые→старые — пересортируем:
|
# Чтобы сервисы шли A→Я а даты новые→старые — пересортируем:
|
||||||
svc_instances.sort(key=lambda i: i.get("instanceConfigDtCreated") or "", reverse=True)
|
svc_instances.sort(key=lambda i: i.get("instanceConfigDtCreated") or i.get("dtCreate") or "", reverse=True)
|
||||||
svc_instances.sort(key=lambda i: i.get("svc", ""))
|
svc_instances.sort(key=lambda i: i.get("svc", ""))
|
||||||
|
|
||||||
# --- Шаг 2: Tracker-fallback ---
|
# --- Шаг 2: Tracker-fallback ---
|
||||||
|
|||||||
@@ -73,13 +73,14 @@ function renderInstances(){
|
|||||||
svcInstances.forEach(i => {
|
svcInstances.forEach(i => {
|
||||||
// Статус: из нашего поля status (cloud-first) или explainedStatus
|
// Статус: из нашего поля status (cloud-first) или explainedStatus
|
||||||
const status = i.status || i.explainedStatus || '?';
|
const status = i.status || i.explainedStatus || '?';
|
||||||
const sc=status==='running'?'badge-success':'';
|
const sc = (status === 'running' || status === 'Выполнен') ? 'badge-success' : '';
|
||||||
|
const created = i.instanceConfigDtCreated || i.dtCreate || '';
|
||||||
// Строка инстанса — кликабельна
|
// Строка инстанса — кликабельна
|
||||||
html += `<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
|
html += `<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
|
||||||
<span style="display:inline-block;width:220px;">${_esc(i.displayName)}</span>
|
<span style="display:inline-block;width:220px;">${_esc(i.displayName)}</span>
|
||||||
<span style="display:inline-block;width:140px;color:var(--muted);font-size:11px;">${_esc(i.svc || '')}</span>
|
<span style="display:inline-block;width:140px;color:var(--muted);font-size:11px;">${_esc(i.svc || '')}</span>
|
||||||
<span class="badge ${sc}" style="font-size:9px;">${_esc(status)}</span>
|
<span class="badge ${sc}" style="font-size:9px;">${_esc(status)}</span>
|
||||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtCreated||''}">${relativeTime(i.instanceConfigDtCreated)}</span>
|
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${created}">${relativeTime(created)}</span>
|
||||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtModified || ''}">${relativeTime(i.instanceConfigDtModified)}</span>
|
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtModified || ''}">${relativeTime(i.instanceConfigDtModified)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
// Контейнер для кнопок операций (изначально скрыт)
|
// Контейнер для кнопок операций (изначально скрыт)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function stopPoll(){
|
|||||||
* Начать создание нового инстанса (кнопка «+ Создать»).
|
* Начать создание нового инстанса (кнопка «+ Создать»).
|
||||||
*
|
*
|
||||||
* Загружает операцию create для текущего сервиса через GET /api/operations/{svcId}.
|
* Загружает операцию create для текущего сервиса через GET /api/operations/{svcId}.
|
||||||
* Если операция create не найдена — fallback на svcOperationId=18 (стандартный).
|
* Числовой ID не зашивается в UI: он берётся из каталога выбранного сервиса.
|
||||||
*/
|
*/
|
||||||
async function startCreate() {
|
async function startCreate() {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
@@ -59,11 +59,17 @@ async function startCreate(){
|
|||||||
try {
|
try {
|
||||||
const r = await fetch('/api/operations/' + currentSvcId);
|
const r = await fetch('/api/operations/' + currentSvcId);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
const createOp=d.operations.find(o=>o.operation==='create');
|
if (!r.ok || d.error) throw new Error(d.error || 'Не удалось загрузить операции сервиса');
|
||||||
const opId=createOp?createOp.svcOperationId:18; // fallback
|
const createOp = (Array.isArray(d.operations) ? d.operations : []).find(o => o.operation === 'create');
|
||||||
showParams(opId,'create');
|
if (!createOp) {
|
||||||
|
document.getElementById('test-status').innerHTML =
|
||||||
|
'<span style="color:var(--destructive);">У выбранного сервиса нет операции create</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showParams(createOp.svcOperationId, 'create');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showParams(18,'create'); // fallback при ошибке
|
document.getElementById('test-status').innerHTML =
|
||||||
|
`<span style="color:var(--destructive);">Ошибка загрузки операций: ${_esc(e.message || '')}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ async function showScenarioEditor(def) {
|
|||||||
service_id: s.service_id || '', operation: s.operation || '',
|
service_id: s.service_id || '', operation: s.operation || '',
|
||||||
output: s.output || '', instance_ref: s.instance_ref || '',
|
output: s.output || '', instance_ref: s.instance_ref || '',
|
||||||
instance_uid: s.instance_uid || '',
|
instance_uid: s.instance_uid || '',
|
||||||
|
_legacyInstanceFallback: s.operation !== 'create' && !s.instance_ref && !s.instance_uid,
|
||||||
|
_showCloud: Boolean(s.instance_uid),
|
||||||
params: s.params || {} // {name: value} — символьные имена
|
params: s.params || {} // {name: value} — символьные имена
|
||||||
})),
|
})),
|
||||||
services: [], cloudInstances: [], opsCache: {}, paramDefsCache: {}
|
services: [], cloudInstances: [], opsCache: {}, paramDefsCache: {}
|
||||||
@@ -58,6 +60,10 @@ async function showScenarioEditor(def) {
|
|||||||
scenarioEditorState.services = [];
|
scenarioEditorState.services = [];
|
||||||
scenarioEditorState.cloudInstances = [];
|
scenarioEditorState.cloudInstances = [];
|
||||||
}
|
}
|
||||||
|
const initialServiceIds = [...new Set(scenarioEditorState.steps
|
||||||
|
.map(step => parseInt(step.service_id, 10))
|
||||||
|
.filter(serviceId => serviceId > 0))];
|
||||||
|
await Promise.all(initialServiceIds.map(loadServiceOperations));
|
||||||
renderEditor();
|
renderEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +144,30 @@ function snapshotAllParams() {
|
|||||||
// Резолв операции + загрузка параметров
|
// Резолв операции + загрузка параметров
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить доступные операции сервиса и сохранить их в кеше редактора.
|
||||||
|
*
|
||||||
|
* @param {number|string} svcId — ID сервиса
|
||||||
|
* @returns {Promise<Array|null>} операции сервиса или null при ошибке загрузки
|
||||||
|
*/
|
||||||
|
async function loadServiceOperations(svcId) {
|
||||||
|
if (!svcId) return null;
|
||||||
|
const st = scenarioEditorState;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(st.opsCache, svcId)) {
|
||||||
|
return st.opsCache[svcId];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/operations/' + svcId);
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok || data.error) return null;
|
||||||
|
st.opsCache[svcId] = Array.isArray(data.operations) ? data.operations : [];
|
||||||
|
return st.opsCache[svcId];
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Найти svcOperationId для операции в шаге.
|
* Найти svcOperationId для операции в шаге.
|
||||||
*
|
*
|
||||||
@@ -179,15 +209,8 @@ async function resolveStepSvcOpId(idx) {
|
|||||||
|
|
||||||
if (!svcId) return null;
|
if (!svcId) return null;
|
||||||
|
|
||||||
// Кеш операций сервиса
|
const ops = await loadServiceOperations(svcId);
|
||||||
if (!st.opsCache[svcId]) {
|
if (!ops) return null;
|
||||||
try {
|
|
||||||
const r = await fetch('/api/operations/' + svcId);
|
|
||||||
const d = await r.json();
|
|
||||||
st.opsCache[svcId] = d.operations || [];
|
|
||||||
} catch (e) { return null; }
|
|
||||||
}
|
|
||||||
const ops = st.opsCache[svcId];
|
|
||||||
const found = ops.find(o => o.operation === op);
|
const found = ops.find(o => o.operation === op);
|
||||||
return found ? found.svcOperationId : null;
|
return found ? found.svcOperationId : null;
|
||||||
}
|
}
|
||||||
@@ -320,13 +343,13 @@ function renderStepRow(idx, step) {
|
|||||||
const st = scenarioEditorState;
|
const st = scenarioEditorState;
|
||||||
const op = step.operation;
|
const op = step.operation;
|
||||||
const out = step.output || '';
|
const out = step.output || '';
|
||||||
const ref = step.instance_ref || '';
|
const ref = step.instance_ref || step.instance_uid || '';
|
||||||
|
|
||||||
// Собрать доступные output-ы из предыдущих create-шагов (для instance_ref select)
|
// Собрать доступные output-ы из предыдущих create-шагов (для instance_ref select)
|
||||||
const prevOutputs = [];
|
const prevOutputs = [];
|
||||||
for (let i = 0; i < idx; i++) {
|
for (let i = 0; i < idx; i++) {
|
||||||
const s = st.steps[i];
|
const s = st.steps[i];
|
||||||
if (s.operation === 'create' && s.output) prevOutputs.push(s.output);
|
if (s.operation === 'create' && s.output && Number(s.service_id) === Number(step.service_id)) prevOutputs.push(s.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div style="border:1px solid var(--brand-gray);border-radius:6px;padding:8px;margin:6px 0;" id="step-${idx}">`;
|
let html = `<div style="border:1px solid var(--brand-gray);border-radius:6px;padding:8px;margin:6px 0;" id="step-${idx}">`;
|
||||||
@@ -342,20 +365,30 @@ function renderStepRow(idx, step) {
|
|||||||
html += `<button class="btn btn-sm" style="font-size:9px;padding:0 4px;margin-left:auto;color:var(--destructive);" onclick="removeStep(${idx})">✕</button>`;
|
html += `<button class="btn btn-sm" style="font-size:9px;padding:0 4px;margin-left:auto;color:var(--destructive);" onclick="removeStep(${idx})">✕</button>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
// Выпадающий список операций
|
// Список операций зависит от актуального каталога выбранного сервиса.
|
||||||
const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
|
|
||||||
let opOpts = '<option value="">— операция —</option>';
|
|
||||||
allOps.forEach(o => { opOpts += `<option value="${o}" ${o == op ? 'selected' : ''}>${o}</option>`; });
|
|
||||||
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;">${opOpts}</select></div>`;
|
|
||||||
|
|
||||||
// Разное содержимое в зависимости от типа операции
|
|
||||||
if (op === 'create') {
|
|
||||||
// CREATE: выбор сервиса + output name
|
|
||||||
let svcOpts = '<option value="">— сервис —</option>';
|
let svcOpts = '<option value="">— сервис —</option>';
|
||||||
st.services.forEach(s => {
|
st.services.forEach(s => {
|
||||||
svcOpts += `<option value="${s.svcId}" ${s.svcId == step.service_id ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
|
svcOpts += `<option value="${s.svcId}" ${s.svcId == step.service_id ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
|
||||||
});
|
});
|
||||||
html += `<div style="display:flex;gap:6px;align-items:center;"><select id="step-${idx}-svc" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].service_id=parseInt(this.value);renderEditor();" style="padding:4px;font-size:12px;">${svcOpts}</select></div>`;
|
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-svc" onchange="onServiceChange(${idx})" style="padding:4px;font-size:12px;">${svcOpts}</select></div>`;
|
||||||
|
|
||||||
|
const svcId = parseInt(step.service_id, 10) || 0;
|
||||||
|
const serviceOps = svcId ? (st.opsCache[svcId] || []) : [];
|
||||||
|
const operationsLoaded = svcId && Object.prototype.hasOwnProperty.call(st.opsCache, svcId);
|
||||||
|
let opOpts = '<option value="">— операция —</option>';
|
||||||
|
serviceOps.filter(item => item.operation !== 'reconcile').forEach(item => {
|
||||||
|
const operationName = item.operation || '';
|
||||||
|
opOpts += `<option value="${_esc(operationName)}" ${operationName === op ? 'selected' : ''}>${_esc(operationName)}</option>`;
|
||||||
|
});
|
||||||
|
if (op && !serviceOps.some(item => item.operation === op)) {
|
||||||
|
opOpts += `<option value="${_esc(op)}" selected>${_esc(op)} (нет в сервисе)</option>`;
|
||||||
|
}
|
||||||
|
const opDisabled = !svcId || !operationsLoaded;
|
||||||
|
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;" ${opDisabled ? 'disabled' : ''}>${opOpts}</select></div>`;
|
||||||
|
|
||||||
|
// Разное содержимое в зависимости от типа операции
|
||||||
|
if (op === 'create') {
|
||||||
|
// CREATE: output name
|
||||||
html += `<div style="margin-top:4px;"><input type="text" id="step-${idx}-output" value="${_esc(out)}" onchange="scenarioEditorState.steps[${idx}].output=this.value" placeholder="имя для ссылок (например: d1)" style="width:220px;padding:3px;font-size:11px;"></div>`;
|
html += `<div style="margin-top:4px;"><input type="text" id="step-${idx}-output" value="${_esc(out)}" onchange="scenarioEditorState.steps[${idx}].output=this.value" placeholder="имя для ссылок (например: d1)" style="width:220px;padding:3px;font-size:11px;"></div>`;
|
||||||
|
|
||||||
} else if (op) {
|
} else if (op) {
|
||||||
@@ -371,13 +404,17 @@ function renderStepRow(idx, step) {
|
|||||||
st.cloudInstances.forEach(i => {
|
st.cloudInstances.forEach(i => {
|
||||||
const dn = i.displayName || '';
|
const dn = i.displayName || '';
|
||||||
// Только autotest-*, не deleted
|
// Только autotest-*, не deleted
|
||||||
if (!dn.startsWith('autotest-') || i.explainedStatus === 'deleted') return;
|
if (!dn.startsWith('autotest-') || i.explainedStatus === 'deleted'
|
||||||
|
|| Number(i.serviceId) !== Number(step.service_id)) return;
|
||||||
const sel = i.instanceUid == ref ? 'selected' : '';
|
const sel = i.instanceUid == ref ? 'selected' : '';
|
||||||
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(dn)} — ${_esc(i.svc || '')} (${_esc(i.explainedStatus || '?')})</option>`;
|
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(dn)} — ${_esc(i.svc || '')} (${_esc(i.explainedStatus || '?')})</option>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (step.instance_uid && !st.cloudInstances.some(i => i.instanceUid === step.instance_uid)) {
|
||||||
|
refOpts += `<option value="${_esc(step.instance_uid)}" selected>☁ ${_esc(step.instance_uid)} (сохранённая ссылка)</option>`;
|
||||||
|
}
|
||||||
const btnLabel = step._showCloud ? '➖ Скрыть облако' : '📥 Из облака';
|
const btnLabel = step._showCloud ? '➖ Скрыть облако' : '📥 Из облака';
|
||||||
html += `<div style="display:flex;gap:4px;align-items:center;"><select id="step-${idx}-ref" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].instance_ref=this.value;renderEditor();" style="padding:4px;font-size:12px;max-width:500px;">${refOpts}</select>`;
|
html += `<div style="display:flex;gap:4px;align-items:center;"><select id="step-${idx}-ref" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].instance_ref=this.value;scenarioEditorState.steps[${idx}].instance_uid='';scenarioEditorState.steps[${idx}]._legacyInstanceFallback=false;renderEditor();" style="padding:4px;font-size:12px;max-width:500px;">${refOpts}</select>`;
|
||||||
html += `<button class="btn btn-sm" style="font-size:10px;padding:0 6px;" onclick="toggleCloudInstances(${idx})">${btnLabel}</button></div>`;
|
html += `<button class="btn btn-sm" style="font-size:10px;padding:0 6px;" onclick="toggleCloudInstances(${idx})">${btnLabel}</button></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +429,24 @@ function renderStepRow(idx, step) {
|
|||||||
// Действия редактора
|
// Действия редактора
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Сменить сервис шага и загрузить его актуальный список операций. */
|
||||||
|
async function onServiceChange(idx) {
|
||||||
|
snapshotAllParams();
|
||||||
|
const step = scenarioEditorState.steps[idx];
|
||||||
|
const svcEl = document.getElementById('step-' + idx + '-svc');
|
||||||
|
const svcId = parseInt(svcEl?.value, 10) || '';
|
||||||
|
step.service_id = svcId;
|
||||||
|
step.operation = '';
|
||||||
|
step.instance_ref = '';
|
||||||
|
step.instance_uid = '';
|
||||||
|
step.params = {};
|
||||||
|
step._svcOpId = null;
|
||||||
|
step._legacyInstanceFallback = false;
|
||||||
|
|
||||||
|
if (svcId) await loadServiceOperations(svcId);
|
||||||
|
renderEditor();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Изменение операции в выпадающем списке → сохранить выбор + сброс + перерисовка.
|
* Изменение операции в выпадающем списке → сохранить выбор + сброс + перерисовка.
|
||||||
*
|
*
|
||||||
@@ -407,6 +462,7 @@ function onOpChange(idx) {
|
|||||||
step.operation = newOp; // сохранить выбранную операцию
|
step.operation = newOp; // сохранить выбранную операцию
|
||||||
step.params = {}; // сброс параметров (для новой операции — другие)
|
step.params = {}; // сброс параметров (для новой операции — другие)
|
||||||
step._svcOpId = null; // сброс кеша svcOperationId
|
step._svcOpId = null; // сброс кеша svcOperationId
|
||||||
|
step._legacyInstanceFallback = false;
|
||||||
|
|
||||||
// Для не-create: автоматически показать облачные инстансы
|
// Для не-create: автоматически показать облачные инстансы
|
||||||
if (newOp && newOp !== 'create') {
|
if (newOp && newOp !== 'create') {
|
||||||
@@ -484,10 +540,11 @@ function collectFormSteps() {
|
|||||||
let svcId = 0;
|
let svcId = 0;
|
||||||
const s = { operation, params: {} };
|
const s = { operation, params: {} };
|
||||||
|
|
||||||
if (operation === 'create') {
|
|
||||||
// CREATE: service_id из select
|
|
||||||
const svcEl = document.getElementById('step-' + idx + '-svc');
|
const svcEl = document.getElementById('step-' + idx + '-svc');
|
||||||
svcId = parseInt(svcEl?.value) || 0;
|
svcId = parseInt(svcEl?.value) || 0;
|
||||||
|
|
||||||
|
if (operation === 'create') {
|
||||||
|
// CREATE: service_id из select + output-ссылка
|
||||||
const outEl = document.getElementById('step-' + idx + '-output');
|
const outEl = document.getElementById('step-' + idx + '-output');
|
||||||
const output = (outEl?.value || '').trim();
|
const output = (outEl?.value || '').trim();
|
||||||
if (output) s.output = output;
|
if (output) s.output = output;
|
||||||
@@ -498,17 +555,12 @@ function collectFormSteps() {
|
|||||||
if (ref) {
|
if (ref) {
|
||||||
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
|
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
|
||||||
if (cloud) {
|
if (cloud) {
|
||||||
svcId = cloud.serviceId;
|
|
||||||
s.instance_uid = ref; // облачный UUID
|
s.instance_uid = ref; // облачный UUID
|
||||||
|
} else if (ref === step.instance_uid) {
|
||||||
|
s.instance_uid = ref; // сохранить UID даже если инстанс уже не в списке
|
||||||
} else {
|
} else {
|
||||||
// output-ссылка — ищем service_id в предыдущих create-шагах
|
// output-ссылка на предыдущий create-шаг
|
||||||
s.instance_ref = ref;
|
s.instance_ref = ref;
|
||||||
for (let j = 0; j < idx; j++) {
|
|
||||||
if (st.steps[j].output === ref) {
|
|
||||||
svcId = st.steps[j].service_id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,15 +602,12 @@ async function saveScenario() {
|
|||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
const s = steps[i];
|
const s = steps[i];
|
||||||
if (!s.operation) { alert('Шаг ' + (i + 1) + ': выберите операцию'); return; }
|
if (!s.operation) { alert('Шаг ' + (i + 1) + ': выберите операцию'); return; }
|
||||||
// CREATE: нужен service_id. Не-CREATE: service_id ИЛИ instance_ref/instance_uid
|
|
||||||
if (s.operation === 'create') {
|
|
||||||
if (!s.service_id) { alert('Шаг ' + (i + 1) + ': выберите сервис'); return; }
|
if (!s.service_id) { alert('Шаг ' + (i + 1) + ': выберите сервис'); return; }
|
||||||
} else {
|
if (s.operation !== 'create' && !s.instance_ref && !s.instance_uid
|
||||||
if (!s.service_id && !s.instance_ref && !s.instance_uid) {
|
&& !scenarioEditorState.steps[i]._legacyInstanceFallback) {
|
||||||
alert('Шаг ' + (i + 1) + ': выберите инстанс (из create-шага или облака)'); return;
|
alert('Шаг ' + (i + 1) + ': выберите инстанс (из create-шага или облака)'); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const st = scenarioEditorState;
|
const st = scenarioEditorState;
|
||||||
const isNew = !st.defId;
|
const isNew = !st.defId;
|
||||||
|
|||||||
+21
-7
@@ -1,4 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -14,17 +16,21 @@ if str(SITE_DIR) not in sys.path:
|
|||||||
|
|
||||||
# Путь к polygon: соседняя репа в корне autotest
|
# Путь к polygon: соседняя репа в корне autotest
|
||||||
POLYGON_DIR = Path(__file__).resolve().parents[2] / "polygon" / "site"
|
POLYGON_DIR = Path(__file__).resolve().parents[2] / "polygon" / "site"
|
||||||
POLYGON_URL = "http://localhost:5000"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app_client():
|
def app_client(polygon_server):
|
||||||
"""Flask test client для app-autotest."""
|
"""Flask test client, направленный в локальный Polygon TEST."""
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
old_polygon_endpoint = app.config.get("POLYGON_ENDPOINT", "")
|
||||||
|
app.config["POLYGON_ENDPOINT"] = f"{polygon_server}/api/v1/svc"
|
||||||
app.config.update(TESTING=True)
|
app.config.update(TESTING=True)
|
||||||
|
try:
|
||||||
with app.test_client() as client:
|
with app.test_client() as client:
|
||||||
yield client
|
yield client
|
||||||
|
finally:
|
||||||
|
app.config["POLYGON_ENDPOINT"] = old_polygon_endpoint
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -37,18 +43,26 @@ def polygon_server():
|
|||||||
if not POLYGON_DIR.is_dir():
|
if not POLYGON_DIR.is_dir():
|
||||||
pytest.skip("polygon repo not found")
|
pytest.skip("polygon repo not found")
|
||||||
|
|
||||||
|
with socket.socket() as port_socket:
|
||||||
|
port_socket.bind(("127.0.0.1", 0))
|
||||||
|
port = port_socket.getsockname()[1]
|
||||||
|
polygon_url = f"http://127.0.0.1:{port}"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["POLYGON_STANDS"] = "test:services/test"
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
["python3", "app.py"],
|
["python3", "-c", f"import app; app.app.run(host='127.0.0.1', port={port}, debug=False)"],
|
||||||
cwd=str(POLYGON_DIR),
|
cwd=str(POLYGON_DIR),
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ждём готовности (poll /health, timeout 10s)
|
# Ждём готовности (poll /health, timeout 10s)
|
||||||
deadline = time.time() + 10
|
deadline = time.time() + 10
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
r = requests.get(f"{POLYGON_URL}/health", timeout=2)
|
r = requests.get(f"{polygon_url}/health", timeout=2)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
break
|
break
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
@@ -58,7 +72,7 @@ def polygon_server():
|
|||||||
proc.wait()
|
proc.wait()
|
||||||
pytest.fail("polygon did not start within 10s")
|
pytest.fail("polygon did not start within 10s")
|
||||||
|
|
||||||
yield POLYGON_URL
|
yield polygon_url
|
||||||
|
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
proc.wait()
|
proc.wait()
|
||||||
@@ -67,5 +81,5 @@ def polygon_server():
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_polygon(polygon_server):
|
def reset_polygon(polygon_server):
|
||||||
"""Перед каждым интеграционным тестом — сброс состояния polygon."""
|
"""Перед каждым интеграционным тестом — сброс состояния polygon."""
|
||||||
requests.post(f"{polygon_server}/api/v1/svc/_mock/reset", timeout=5)
|
requests.post(f"{polygon_server}/test/api/v1/svc/_mock/reset", timeout=5)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
# ── Хелперы ──
|
# ── Хелперы ──
|
||||||
@@ -48,6 +49,46 @@ class TestScenarioLifecycle:
|
|||||||
data = resp.get_json()
|
data = resp.get_json()
|
||||||
assert isinstance(data, list)
|
assert isinstance(data, list)
|
||||||
|
|
||||||
|
def test_manual_create_delete_uses_local_polygon(self, app_client, monkeypatch):
|
||||||
|
"""Ручной API приложения проводит create и delete через локальный Polygon."""
|
||||||
|
from api.http_client import HttpClient
|
||||||
|
from routes import api_test as api_test_routes
|
||||||
|
|
||||||
|
monkeypatch.setattr(api_test_routes, "_finish_op", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
HttpClient,
|
||||||
|
"raw_delete",
|
||||||
|
lambda *args, **kwargs: pytest.fail("Polygon delete must not call external CMDB"),
|
||||||
|
)
|
||||||
|
|
||||||
|
operations_response = app_client.get("/api/operations/1")
|
||||||
|
assert operations_response.status_code == 200
|
||||||
|
operations = operations_response.get_json()["operations"]
|
||||||
|
create_id = next(op["svcOperationId"] for op in operations if op["operation"] == "create")
|
||||||
|
delete_id = next(op["svcOperationId"] for op in operations if op["operation"] == "delete")
|
||||||
|
|
||||||
|
created = app_client.post("/api/test", json={
|
||||||
|
"serviceId": 1,
|
||||||
|
"operation": "create",
|
||||||
|
"svcOperationId": create_id,
|
||||||
|
"params": {},
|
||||||
|
"displayName": "autotest-local-integration",
|
||||||
|
})
|
||||||
|
assert created.status_code == 200, created.get_json()
|
||||||
|
create_result = app_client.get(f"/api/test/status/{created.get_json()['opUid']}").get_json()
|
||||||
|
assert create_result["status"] == "OK"
|
||||||
|
|
||||||
|
deleted = app_client.post("/api/test", json={
|
||||||
|
"serviceId": 1,
|
||||||
|
"operation": "delete",
|
||||||
|
"svcOperationId": delete_id,
|
||||||
|
"instanceUid": created.get_json()["instanceUid"],
|
||||||
|
"params": {},
|
||||||
|
})
|
||||||
|
assert deleted.status_code == 200, deleted.get_json()
|
||||||
|
delete_result = app_client.get(f"/api/test/status/{deleted.get_json()['opUid']}").get_json()
|
||||||
|
assert delete_result["status"] == "OK"
|
||||||
|
|
||||||
def test_create_and_run_simple_scenario(self, app_client):
|
def test_create_and_run_simple_scenario(self, app_client):
|
||||||
"""Сценарий create -> delete. Проверяем что оба шага OK."""
|
"""Сценарий create -> delete. Проверяем что оба шага OK."""
|
||||||
steps = [
|
steps = [
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
Бьёт в РАЗВЁРНУТЫЙ автотест (atest.pythonk8s.dev.nubes.ru), эмуляция TEST.
|
Бьёт в РАЗВЁРНУТЫЙ автотест (atest.pythonk8s.dev.nubes.ru), эмуляция TEST.
|
||||||
Операции идут через app-autotest -> executor -> save_run -> история в БД.
|
Операции идут через app-autotest -> executor -> save_run -> история в БД.
|
||||||
"""
|
"""
|
||||||
import time, requests
|
import time, requests, pytest
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skip(reason="live test against deployed atest service; run this file directly")
|
||||||
|
|
||||||
BASE = "https://atest.pythonk8s.dev.nubes.ru"
|
BASE = "https://atest.pythonk8s.dev.nubes.ru"
|
||||||
COOKIES = {"mode": "polygon", "polygon_stand": "test"}
|
COOKIES = {"mode": "polygon", "polygon_stand": "test"}
|
||||||
@@ -19,8 +21,13 @@ def test_services():
|
|||||||
|
|
||||||
|
|
||||||
def test_create(svc_id):
|
def test_create(svc_id):
|
||||||
|
operations_response = requests.get(f"{BASE}/api/operations/{svc_id}", cookies=COOKIES, timeout=10)
|
||||||
|
assert operations_response.status_code == 200
|
||||||
|
operations = operations_response.json().get("operations", [])
|
||||||
|
create_op = next((op for op in operations if op.get("operation") == "create"), None)
|
||||||
|
assert create_op is not None, f"No create operation for service {svc_id}"
|
||||||
r = requests.post(f"{BASE}/api/test",
|
r = requests.post(f"{BASE}/api/test",
|
||||||
json={"serviceId": svc_id, "operation": "create", "svcOperationId": 18,
|
json={"serviceId": svc_id, "operation": "create", "svcOperationId": create_op["svcOperationId"],
|
||||||
"params": {}, "displayName": f"autotest-smoke-{svc_id}"},
|
"params": {}, "displayName": f"autotest-smoke-{svc_id}"},
|
||||||
cookies=COOKIES, timeout=30)
|
cookies=COOKIES, timeout=30)
|
||||||
assert r.status_code == 200, f"Create failed: {r.text[:200]}"
|
assert r.status_code == 200, f"Create failed: {r.text[:200]}"
|
||||||
|
|||||||
@@ -21,3 +21,44 @@ def test_scenario_name_escape_chain_includes_amp_backslash_quote_doublequote():
|
|||||||
".replace(/\"/g,'"');"
|
".replace(/\"/g,'"');"
|
||||||
)
|
)
|
||||||
assert expected in content
|
assert expected in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenario_editor_loads_operations_from_selected_service():
|
||||||
|
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "scenario-form.js"
|
||||||
|
content = js_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "async function loadServiceOperations(svcId)" in content
|
||||||
|
assert "fetch('/api/operations/' + svcId)" in content
|
||||||
|
assert "serviceOps.filter(item => item.operation !== 'reconcile')" in content
|
||||||
|
assert "const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy']" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_create_uses_current_service_operation_id():
|
||||||
|
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "operations.js"
|
||||||
|
content = js_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "find(o => o.operation === 'create')" in content
|
||||||
|
assert "showParams(createOp.svcOperationId, 'create')" in content
|
||||||
|
assert "showParams(18, 'create')" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenario_editor_preserves_legacy_and_cloud_instance_references():
|
||||||
|
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "scenario-form.js"
|
||||||
|
content = js_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "_legacyInstanceFallback: s.operation !== 'create'" in content
|
||||||
|
assert "_showCloud: Boolean(s.instance_uid)" in content
|
||||||
|
assert "ref === step.instance_uid" in content
|
||||||
|
assert "s.instance_uid = ref" in content
|
||||||
|
assert "&& !scenarioEditorState.steps[i]._legacyInstanceFallback" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_live_scripts_are_not_collected_as_network_tests():
|
||||||
|
tests_dir = Path(__file__).resolve().parent
|
||||||
|
user_simulation = (tests_dir / "test_user_simulation.py").read_text(encoding="utf-8")
|
||||||
|
smoke = (tests_dir / "test_smoke_all_services.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "pytest.skip(" in user_simulation
|
||||||
|
assert "pytestmark = pytest.mark.skip" in smoke
|
||||||
|
assert "operation_id(sid, \"create\"" in user_simulation
|
||||||
|
assert "create_op[\"svcOperationId\"]" in smoke
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
ручные операции + сценарии + переключения + краевые случаи.
|
ручные операции + сценарии + переключения + краевые случаи.
|
||||||
Запуск: python3 tests/test_user_simulation.py
|
Запуск: python3 tests/test_user_simulation.py
|
||||||
"""
|
"""
|
||||||
import time, requests, json, sys
|
import time, requests, json, sys, pytest
|
||||||
|
|
||||||
|
if __name__ != "__main__":
|
||||||
|
pytest.skip("live test against deployed atest service; run this file directly", allow_module_level=True)
|
||||||
|
|
||||||
BASE = "https://atest.pythonk8s.dev.nubes.ru"
|
BASE = "https://atest.pythonk8s.dev.nubes.ru"
|
||||||
PASS = FAIL = 0
|
PASS = FAIL = 0
|
||||||
@@ -31,6 +34,14 @@ def post(path, data, cookies=None, code=200):
|
|||||||
assert r.status_code == code, f"POST {path}: {r.status_code} {r.text[:100]}"
|
assert r.status_code == code, f"POST {path}: {r.status_code} {r.text[:100]}"
|
||||||
return r.json() if r.text.strip() else {}
|
return r.json() if r.text.strip() else {}
|
||||||
|
|
||||||
|
def operation_id(service_id, operation, cookies=None):
|
||||||
|
operations = get(f"/api/operations/{service_id}", cookies)
|
||||||
|
match = next((item for item in operations.get("operations", [])
|
||||||
|
if item.get("operation") == operation), None)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError(f"Operation {operation!r} not found for service {service_id}")
|
||||||
|
return match["svcOperationId"]
|
||||||
|
|
||||||
def poll(op_uid, cookies=None, timeout=15):
|
def poll(op_uid, cookies=None, timeout=15):
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -42,7 +53,7 @@ def poll(op_uid, cookies=None, timeout=15):
|
|||||||
|
|
||||||
def create_instance(sid, name=None, cookies=None):
|
def create_instance(sid, name=None, cookies=None):
|
||||||
r = post("/api/test",
|
r = post("/api/test",
|
||||||
{"serviceId": sid, "operation": "create", "svcOperationId": 18,
|
{"serviceId": sid, "operation": "create", "svcOperationId": operation_id(sid, "create", cookies),
|
||||||
"params": {}, "displayName": name or f"autotest-user-{sid}"},
|
"params": {}, "displayName": name or f"autotest-user-{sid}"},
|
||||||
cookies)
|
cookies)
|
||||||
return r["opUid"], r.get("instanceUid")
|
return r["opUid"], r.get("instanceUid")
|
||||||
@@ -249,7 +260,7 @@ try:
|
|||||||
# 9.1 Создать PostgreSQL
|
# 9.1 Создать PostgreSQL
|
||||||
pg_name = f"autotest-pg-{int(time.time())}"
|
pg_name = f"autotest-pg-{int(time.time())}"
|
||||||
r = post("/api/test",
|
r = post("/api/test",
|
||||||
{"serviceId": 90, "operation": "create", "svcOperationId": 19,
|
{"serviceId": 90, "operation": "create", "svcOperationId": operation_id(90, "create"),
|
||||||
"params": {}, "displayName": pg_name})
|
"params": {}, "displayName": pg_name})
|
||||||
pg_op = r["opUid"]
|
pg_op = r["opUid"]
|
||||||
pg_uid = r.get("instanceUid")
|
pg_uid = r.get("instanceUid")
|
||||||
@@ -260,7 +271,7 @@ try:
|
|||||||
# 9.2 Создать пользователя
|
# 9.2 Создать пользователя
|
||||||
user_name = f"testuser_{int(time.time()) % 10000}"
|
user_name = f"testuser_{int(time.time()) % 10000}"
|
||||||
r = post("/api/test",
|
r = post("/api/test",
|
||||||
{"serviceId": 90, "operation": "create_user", "svcOperationId": 241,
|
{"serviceId": 90, "operation": "create_user", "svcOperationId": operation_id(90, "create_user"),
|
||||||
"instanceUid": pg_uid,
|
"instanceUid": pg_uid,
|
||||||
"params": {"733": user_name, "740": "app_user", "1096": "false"}})
|
"params": {"733": user_name, "740": "app_user", "1096": "false"}})
|
||||||
user_op = r["opUid"]
|
user_op = r["opUid"]
|
||||||
@@ -270,7 +281,7 @@ try:
|
|||||||
# 9.3 Создать базу данных
|
# 9.3 Создать базу данных
|
||||||
db_name = f"testdb_{int(time.time()) % 10000}"
|
db_name = f"testdb_{int(time.time()) % 10000}"
|
||||||
r = post("/api/test",
|
r = post("/api/test",
|
||||||
{"serviceId": 90, "operation": "create_database", "svcOperationId": 245,
|
{"serviceId": 90, "operation": "create_database", "svcOperationId": operation_id(90, "create_database"),
|
||||||
"instanceUid": pg_uid,
|
"instanceUid": pg_uid,
|
||||||
"params": {"741": db_name, "742": user_name}})
|
"params": {"741": db_name, "742": user_name}})
|
||||||
db_op = r["opUid"]
|
db_op = r["opUid"]
|
||||||
@@ -279,7 +290,7 @@ try:
|
|||||||
|
|
||||||
# 9.4 Удалить PostgreSQL
|
# 9.4 Удалить PostgreSQL
|
||||||
r = post("/api/test",
|
r = post("/api/test",
|
||||||
{"serviceId": 90, "operation": "delete", "svcOperationId": 20,
|
{"serviceId": 90, "operation": "delete", "svcOperationId": operation_id(90, "delete"),
|
||||||
"instanceUid": pg_uid, "params": {}})
|
"instanceUid": pg_uid, "params": {}})
|
||||||
del_op = r["opUid"]
|
del_op = r["opUid"]
|
||||||
result = poll(del_op)
|
result = poll(del_op)
|
||||||
|
|||||||
Reference in New Issue
Block a user