358 lines
11 KiB
JavaScript
358 lines
11 KiB
JavaScript
const form = document.getElementById('createForm');
|
|
const nodeEl = document.getElementById('node');
|
|
const storageEl = document.getElementById('storage');
|
|
const templateEl = document.getElementById('template');
|
|
const vmidEl = document.getElementById('vmid');
|
|
const ipModeEl = document.getElementById('ip_mode');
|
|
const refreshVmidBtn = document.getElementById('refreshVmid');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const taskProgressBar = document.getElementById('taskProgressBar');
|
|
const taskStage = document.getElementById('taskStage');
|
|
const taskLogs = document.getElementById('taskLogs');
|
|
const containerDetails = document.getElementById('containerDetails');
|
|
const globalAlert = document.getElementById('globalAlert');
|
|
|
|
let taskPollTimer = null;
|
|
|
|
function showAlert(message, level = 'danger') {
|
|
globalAlert.innerHTML = `<div class="alert alert-${level} py-2">${message}</div>`;
|
|
}
|
|
|
|
function clearAlert() {
|
|
globalAlert.innerHTML = '';
|
|
}
|
|
|
|
function setFieldError(name, message = '') {
|
|
const target = document.querySelector(`[data-error-for="${name}"]`);
|
|
const field = form.querySelector(`[name="${name}"]`);
|
|
if (target) target.textContent = message || '';
|
|
if (field) field.classList.toggle('is-invalid', Boolean(message));
|
|
}
|
|
|
|
function clearErrors() {
|
|
form.querySelectorAll('.is-invalid').forEach((el) => el.classList.remove('is-invalid'));
|
|
form.querySelectorAll('[data-error-for]').forEach((el) => {
|
|
el.textContent = '';
|
|
});
|
|
}
|
|
|
|
async function parseResponse(response, url) {
|
|
const text = await response.text();
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (e) {
|
|
throw new Error(`Server nevrátil JSON pro ${url}.`);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
async function apiGet(url) {
|
|
const response = await fetch(url, {
|
|
credentials: 'same-origin',
|
|
});
|
|
|
|
const data = await parseResponse(response, url);
|
|
|
|
if (!response.ok || data.success === false) {
|
|
throw new Error(data.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
async function apiPost(url, payload) {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const data = await parseResponse(response, url);
|
|
|
|
if (!response.ok || data.success === false) {
|
|
throw new Error(data.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function option(text, value) {
|
|
const opt = document.createElement('option');
|
|
opt.value = value;
|
|
opt.textContent = text;
|
|
return opt;
|
|
}
|
|
|
|
async function loadNodes() {
|
|
const data = await apiGet('/api/nodes');
|
|
nodeEl.innerHTML = '';
|
|
|
|
data.nodes.forEach((node) => {
|
|
nodeEl.appendChild(option(node, node));
|
|
});
|
|
|
|
if (window.APP_DEFAULTS?.defaultNode && data.nodes.includes(window.APP_DEFAULTS.defaultNode)) {
|
|
nodeEl.value = window.APP_DEFAULTS.defaultNode;
|
|
}
|
|
}
|
|
|
|
async function loadStorage() {
|
|
if (!nodeEl.value) {
|
|
storageEl.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const data = await apiGet(`/api/storage/${encodeURIComponent(nodeEl.value)}`);
|
|
storageEl.innerHTML = '';
|
|
|
|
data.storage.forEach((item) => {
|
|
storageEl.appendChild(option(item, item));
|
|
});
|
|
|
|
if (window.APP_DEFAULTS?.defaultStorage && data.storage.includes(window.APP_DEFAULTS.defaultStorage)) {
|
|
storageEl.value = window.APP_DEFAULTS.defaultStorage;
|
|
}
|
|
}
|
|
|
|
async function loadTemplates() {
|
|
templateEl.innerHTML = '';
|
|
|
|
if (!nodeEl.value || !storageEl.value) {
|
|
return;
|
|
}
|
|
|
|
const data = await apiGet(
|
|
`/api/templates/${encodeURIComponent(nodeEl.value)}/${encodeURIComponent(storageEl.value)}`
|
|
);
|
|
|
|
data.templates.forEach((item) => {
|
|
templateEl.appendChild(option(item, item));
|
|
});
|
|
}
|
|
|
|
async function loadNextId() {
|
|
const data = await apiGet('/api/next-vmid');
|
|
vmidEl.value = data.vmid;
|
|
}
|
|
|
|
function toggleStaticIpFields() {
|
|
const staticOnly = document.querySelectorAll('.static-ip-only');
|
|
const isStatic = ipModeEl.value === 'static';
|
|
staticOnly.forEach((el) => el.classList.toggle('d-none', !isStatic));
|
|
}
|
|
|
|
function collectFormData() {
|
|
return {
|
|
node: nodeEl.value,
|
|
storage: storageEl.value,
|
|
template: templateEl.value,
|
|
vmid: vmidEl.value,
|
|
hostname: document.getElementById('hostname').value,
|
|
bridge: document.getElementById('bridge').value,
|
|
cores: document.getElementById('cores').value,
|
|
memory: document.getElementById('memory').value,
|
|
disk: document.getElementById('disk_gb').value,
|
|
ip_mode: document.getElementById('ip_mode').value,
|
|
ip_last_octet: document.getElementById('ip_host').value,
|
|
vlan: document.getElementById('vlan_tag').value,
|
|
gateway: document.getElementById('gateway').value,
|
|
nameserver: document.getElementById('dns').value,
|
|
searchdomain: document.getElementById('searchdomain').value,
|
|
password: document.getElementById('password').value,
|
|
start_on_boot: document.getElementById('onboot').checked,
|
|
start_now: document.getElementById('start_now').checked,
|
|
unprivileged: document.getElementById('unprivileged').checked,
|
|
enable_nesting: document.getElementById('enable_nesting').checked,
|
|
};
|
|
}
|
|
|
|
function renderContainerDetails(container) {
|
|
containerDetails.innerHTML = `
|
|
<dl class="row small mb-0">
|
|
<dt class="col-5">Hostname</dt><dd class="col-7">${container.hostname || '-'}</dd>
|
|
<dt class="col-5">VMID</dt><dd class="col-7">${container.vmid || '-'}</dd>
|
|
<dt class="col-5">Node</dt><dd class="col-7">${container.node || '-'}</dd>
|
|
<dt class="col-5">Stav</dt><dd class="col-7">${container.status || '-'}</dd>
|
|
<dt class="col-5">CPU</dt><dd class="col-7">${container.cores || '-'}</dd>
|
|
<dt class="col-5">RAM</dt><dd class="col-7">${container.memory || '-'} MB</dd>
|
|
<dt class="col-5">RootFS</dt><dd class="col-7 text-break">${container.rootfs || '-'}</dd>
|
|
<dt class="col-5">Síť</dt><dd class="col-7 text-break">${container.net0 || '-'}</dd>
|
|
<dt class="col-5">DNS</dt><dd class="col-7">${container.nameserver || '-'}</dd>
|
|
<dt class="col-5">Search domain</dt><dd class="col-7">${container.searchdomain || '-'}</dd>
|
|
<dt class="col-5">Template</dt><dd class="col-7 text-break">${container.ostemplate || '-'}</dd>
|
|
</dl>
|
|
`;
|
|
}
|
|
|
|
function updateProgress(progress, text, logs = [], variant = 'primary') {
|
|
taskProgressBar.style.width = `${progress}%`;
|
|
taskProgressBar.textContent = `${progress}%`;
|
|
taskProgressBar.className = `progress-bar ${progress < 100 ? 'progress-bar-striped progress-bar-animated' : ''} bg-${variant}`.trim();
|
|
taskStage.textContent = text;
|
|
taskLogs.textContent = logs.length ? logs.join('\n') : '-';
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (taskPollTimer) {
|
|
clearTimeout(taskPollTimer);
|
|
taskPollTimer = null;
|
|
}
|
|
}
|
|
|
|
async function loadContainerDetails(node, vmid) {
|
|
const data = await apiGet(`/api/container-details/${encodeURIComponent(node)}/${encodeURIComponent(vmid)}`);
|
|
if (data.details) {
|
|
renderContainerDetails(data.details);
|
|
}
|
|
}
|
|
|
|
async function startContainerNow(node, vmid) {
|
|
return apiPost('/api/start-lxc', { node, vmid });
|
|
}
|
|
|
|
async function pollTask(node, upid) {
|
|
const data = await apiGet(
|
|
`/api/task-status?node=${encodeURIComponent(node)}&upid=${encodeURIComponent(upid)}`
|
|
);
|
|
|
|
const failed = !data.running && data.exitstatus && data.exitstatus !== 'OK';
|
|
|
|
let stageText = 'Úloha běží...';
|
|
if (!data.running && data.exitstatus === 'OK') {
|
|
stageText = 'Úloha byla úspěšně dokončena.';
|
|
} else if (!data.running && data.exitstatus && data.exitstatus !== 'OK') {
|
|
stageText = `Úloha skončila chybou: ${data.exitstatus}`;
|
|
}
|
|
|
|
updateProgress(
|
|
data.progress ?? 0,
|
|
stageText,
|
|
data.logs || [],
|
|
failed ? 'danger' : (data.progress === 100 && data.exitstatus === 'OK' ? 'success' : 'primary')
|
|
);
|
|
|
|
return data;
|
|
}
|
|
|
|
async function waitForTask(node, upid) {
|
|
while (true) {
|
|
const data = await pollTask(node, upid);
|
|
|
|
if (!data.running) {
|
|
return data;
|
|
}
|
|
|
|
await new Promise((resolve) => {
|
|
taskPollTimer = setTimeout(resolve, 2000);
|
|
});
|
|
}
|
|
}
|
|
|
|
nodeEl.addEventListener('change', async () => {
|
|
clearAlert();
|
|
try {
|
|
await loadStorage();
|
|
await loadTemplates();
|
|
} catch (error) {
|
|
showAlert(error.message);
|
|
}
|
|
});
|
|
|
|
storageEl.addEventListener('change', async () => {
|
|
clearAlert();
|
|
try {
|
|
await loadTemplates();
|
|
} catch (error) {
|
|
showAlert(error.message);
|
|
}
|
|
});
|
|
|
|
ipModeEl.addEventListener('change', toggleStaticIpFields);
|
|
|
|
refreshVmidBtn.addEventListener('click', async () => {
|
|
try {
|
|
await loadNextId();
|
|
} catch (error) {
|
|
showAlert(error.message);
|
|
}
|
|
});
|
|
|
|
form.addEventListener('reset', () => {
|
|
stopPolling();
|
|
clearErrors();
|
|
clearAlert();
|
|
|
|
setTimeout(() => {
|
|
toggleStaticIpFields();
|
|
updateProgress(0, 'Ještě nebyla spuštěna žádná úloha.', []);
|
|
containerDetails.textContent = 'Po úspěšném vytvoření se tady zobrazí detaily.';
|
|
}, 0);
|
|
});
|
|
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
stopPolling();
|
|
clearErrors();
|
|
clearAlert();
|
|
submitBtn.disabled = true;
|
|
containerDetails.textContent = 'Čekám na dokončení vytváření kontejneru.';
|
|
updateProgress(8, 'Odesílám požadavek do Proxmoxu...', []);
|
|
|
|
try {
|
|
const payload = collectFormData();
|
|
const startNow = payload.start_now;
|
|
|
|
const createData = await apiPost('/api/create-lxc', payload);
|
|
|
|
showAlert(`Úloha byla spuštěna. VMID ${createData.vmid}, hostname ${createData.hostname}.`, 'info');
|
|
updateProgress(12, 'Vytváření kontejneru bylo přijato Proxmoxem.', []);
|
|
|
|
const createTaskResult = await waitForTask(createData.node, createData.upid);
|
|
|
|
if (createTaskResult.exitstatus !== 'OK') {
|
|
submitBtn.disabled = false;
|
|
showAlert(`Vytvoření kontejneru selhalo: ${createTaskResult.exitstatus || 'neznámá chyba'}`, 'danger');
|
|
return;
|
|
}
|
|
|
|
if (startNow) {
|
|
updateProgress(90, 'Spouštím nově vytvořený kontejner...', createTaskResult.logs || []);
|
|
const startData = await startContainerNow(createData.node, createData.vmid);
|
|
const startTaskResult = await waitForTask(startData.node, startData.upid);
|
|
|
|
if (startTaskResult.exitstatus !== 'OK') {
|
|
submitBtn.disabled = false;
|
|
showAlert(`Kontejner byl vytvořen, ale nepodařilo se ho spustit: ${startTaskResult.exitstatus || 'neznámá chyba'}`, 'warning');
|
|
await loadContainerDetails(createData.node, createData.vmid);
|
|
return;
|
|
}
|
|
}
|
|
|
|
updateProgress(100, 'Kontejner byl úspěšně vytvořen.', [], 'success');
|
|
showAlert(`Kontejner VMID ${createData.vmid} byl úspěšně vytvořen.`, 'success');
|
|
await loadContainerDetails(createData.node, createData.vmid);
|
|
submitBtn.disabled = false;
|
|
} catch (error) {
|
|
submitBtn.disabled = false;
|
|
showAlert(error.message);
|
|
updateProgress(100, `Požadavek selhal: ${error.message}`, [], 'danger');
|
|
}
|
|
});
|
|
|
|
(async function init() {
|
|
toggleStaticIpFields();
|
|
|
|
try {
|
|
await loadNodes();
|
|
await loadStorage();
|
|
await loadTemplates();
|
|
await loadNextId();
|
|
} catch (error) {
|
|
showAlert(error.message);
|
|
}
|
|
})();
|