Compare commits
2 Commits
4ef79112be
...
cd48c839f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd48c839f8 | ||
|
|
06ea2b5bd4 |
@@ -268,12 +268,14 @@ async def create_backup(
|
||||
compress = data.get("compress", "zstd")
|
||||
remove = int(data.get("remove", 1))
|
||||
notes = data.get("notes")
|
||||
keep_last = data.get("keep_last")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
result = client.create_backup(
|
||||
node=node, vmid=int(vmid), storage=storage,
|
||||
mode=mode, compress=compress, remove=remove, notes=notes,
|
||||
mode=mode, compress=compress, remove=remove,
|
||||
keep_last=keep_last, notes=notes,
|
||||
)
|
||||
if result.get("success"):
|
||||
LoggingService.log_proxmox_action(
|
||||
|
||||
@@ -1649,13 +1649,6 @@ async def vnc_websocket_proxy(
|
||||
if auth_ticket:
|
||||
logger.info(f"Using auth_ticket from frontend for VNC WebSocket")
|
||||
|
||||
# Отправляем пароль клиенту первым сообщением (для noVNC credentials).
|
||||
# В Proxmox 9.x generate-password убран — VNC использует NoAuth (Type 1),
|
||||
# поэтому отправляем пустую строку, чтобы onFirstMessage на фронтенде всегда срабатывал.
|
||||
password_to_send = vnc_password if vnc_password else ""
|
||||
logger.info(f"Sending VNC password to client as first message (len={len(password_to_send)})")
|
||||
await websocket.send_text(password_to_send)
|
||||
|
||||
# Построить URL для Proxmox WebSocket
|
||||
# Преобразуем vmtype: vm -> qemu, container -> lxc
|
||||
proxmox_vmtype = "qemu" if vmtype == "vm" else "lxc"
|
||||
@@ -1725,7 +1718,7 @@ async def vnc_websocket_proxy(
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Client to Proxmox ended: {e}")
|
||||
logger.info(f"Client to Proxmox ended: {e}")
|
||||
|
||||
async def proxmox_to_client():
|
||||
"""Пересылка данных от Proxmox к клиенту"""
|
||||
@@ -1739,10 +1732,11 @@ async def vnc_websocket_proxy(
|
||||
else:
|
||||
bytes_from_proxmox += len(message)
|
||||
await websocket.send_text(message)
|
||||
except Exception:
|
||||
except Exception as send_err:
|
||||
logger.info(f"Send to client failed: {send_err}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Proxmox to client ended: {e}")
|
||||
logger.info(f"Proxmox to client ended: {e}")
|
||||
|
||||
# Запускаем обе задачи
|
||||
done, pending = await asyncio.wait(
|
||||
@@ -1760,7 +1754,7 @@ async def vnc_websocket_proxy(
|
||||
except Exception as e:
|
||||
logger.error(f"VNC WebSocket proxy error: {e}")
|
||||
finally:
|
||||
logger.debug(f"VNC stats - To Proxmox: {bytes_to_proxmox} bytes, From Proxmox: {bytes_from_proxmox} bytes")
|
||||
logger.info(f"VNC stats - To Proxmox: {bytes_to_proxmox} bytes, From Proxmox: {bytes_from_proxmox} bytes")
|
||||
if proxmox_ws:
|
||||
await proxmox_ws.close()
|
||||
try:
|
||||
|
||||
@@ -3180,7 +3180,7 @@ class ProxmoxClient:
|
||||
if keep_last and int(keep_last) > 0:
|
||||
params["prune-backups"] = f"keep-last={int(keep_last)}"
|
||||
if notes:
|
||||
params["notes"] = notes
|
||||
params["notes-template"] = notes
|
||||
upid = self.proxmox.nodes(node).vzdump.post(**params)
|
||||
return {"success": True, "upid": upid}
|
||||
except Exception as e:
|
||||
|
||||
@@ -1272,95 +1272,73 @@ async function openVNC() {
|
||||
// Очистить контейнер
|
||||
vncContent.innerHTML = '';
|
||||
|
||||
// Создать URL для websocket подключения через наш прокси
|
||||
// Создать URL для websocket подключения через наш прокси (передаём напрямую в noVNC)
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
// Backend expects vm/container, not qemu/lxc
|
||||
const resourceType = vncData.type === 'qemu' ? 'vm' : 'container';
|
||||
|
||||
// Подключаемся через наш WebSocket прокси (используем node из ответа API)
|
||||
// Передаём vnc_password для получения его первым сообщением
|
||||
const wsUrl = `${protocol}//${wsHost}/proxmox/ws/vnc/${serverId}/${vncData.node}/${resourceType}/${vmid}?port=${vncData.port}&vncticket=${encodeURIComponent(vncData.ticket)}&auth_ticket=${encodeURIComponent(vncData.auth_ticket)}&vnc_password=${encodeURIComponent(vncData.password || '')}`;
|
||||
const wsUrl = `${protocol}//${wsHost}/proxmox/ws/vnc/${serverId}/${vncData.node}/${resourceType}/${vmid}?port=${vncData.port}&vncticket=${encodeURIComponent(vncData.ticket)}&auth_ticket=${encodeURIComponent(vncData.auth_ticket)}`;
|
||||
|
||||
// Создаём WebSocket вручную и ждём первое сообщение с паролем
|
||||
const ws = new WebSocket(wsUrl);
|
||||
const vncPassword = vncData.password || '';
|
||||
|
||||
ws.addEventListener('message', function onFirstMessage(event) {
|
||||
// Первое сообщение - это VNC пароль от бэкенда
|
||||
const vncPassword = event.data;
|
||||
|
||||
// Убираем этот обработчик (нужен только для первого сообщения)
|
||||
ws.removeEventListener('message', onFirstMessage);
|
||||
|
||||
// Создаём RFB с существующим WebSocket и полученным паролем
|
||||
vncConnection = new RFBClass(vncContent, ws, {
|
||||
credentials: { password: vncPassword }
|
||||
});
|
||||
|
||||
vncConnection.scaleViewport = true;
|
||||
vncConnection.resizeSession = true;
|
||||
|
||||
vncConnection.addEventListener('connect', () => {
|
||||
setTimeout(() => {
|
||||
if (vncConnection && vncConnection._display) {
|
||||
vncConnection._display.autoscale(vncContent.clientWidth, vncContent.clientHeight);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('credentialsrequired', (e) => {
|
||||
vncConnection.sendCredentials({ password: vncPassword });
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('disconnect', (e) => {
|
||||
if (e.detail.clean) {
|
||||
vncContent.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary);">${t('connection_closed')}</div>`;
|
||||
} else {
|
||||
vncContent.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); padding: 2rem;">
|
||||
<div style="font-size: 2rem; margin-bottom: 1rem;"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div style="margin-bottom: 1rem; font-size: 1.125rem;">${t('connection_interrupted')}</div>
|
||||
<div style="background: var(--bg-tertiary); padding: 1.5rem; border-radius: 8px; max-width: 500px; text-align: left; margin-bottom: 1rem;">
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 0.5rem;">
|
||||
<strong>${t('possible_reasons')}:</strong>
|
||||
</p>
|
||||
<ul style="color: var(--text-muted); font-size: 0.8rem; margin-left: 1rem;">
|
||||
<li>${t('vnc_requires_password')}</li>
|
||||
<li>${t('vnc_add_password')}</li>
|
||||
<li>${t('vnc_vm_may_be_off')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<button onclick="openVNC()" class="btn btn-primary"><i class="fa-solid fa-rotate"></i> ${t('reconnect')}</button>
|
||||
<a href="https://${vncData.host}:8006" target="_blank" class="btn btn-secondary"><i class="fa-solid fa-globe"></i> ${t('open_proxmox')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('securityfailure', (e) => {
|
||||
vncContent.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<div style="font-size: 2rem; margin-bottom: 1rem;"><i class="fa-solid fa-lock"></i></div>
|
||||
<div class="text-danger">${t('vnc_security_error')}</div>
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem; margin-top: 0.5rem;">${e.detail.reason || t('unknown_error')}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}, { once: true });
|
||||
|
||||
ws.addEventListener('error', (error) => {
|
||||
vncContent.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--text-secondary);"><i class="fa-solid fa-circle-xmark"></i> WebSocket connection error</div>`;
|
||||
// noVNC управляет WebSocket самостоятельно через URL
|
||||
vncConnection = new RFBClass(vncContent, wsUrl, {
|
||||
credentials: { password: vncPassword }
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
if (!vncConnection) {
|
||||
// Закрыто до создания RFB
|
||||
vncContent.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--text-secondary);"><i class="fa-solid fa-circle-xmark"></i> Connection closed: ${event.reason || 'Unknown error'}</div>`;
|
||||
vncConnection.scaleViewport = true;
|
||||
vncConnection.resizeSession = true;
|
||||
|
||||
vncConnection.addEventListener('connect', () => {
|
||||
setTimeout(() => {
|
||||
if (vncConnection && vncConnection._display) {
|
||||
vncConnection._display.autoscale(vncContent.clientWidth, vncContent.clientHeight);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('credentialsrequired', (e) => {
|
||||
vncConnection.sendCredentials({ password: vncPassword });
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('disconnect', (e) => {
|
||||
if (e.detail.clean) {
|
||||
vncContent.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary);">${t('connection_closed')}</div>`;
|
||||
} else {
|
||||
vncContent.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); padding: 2rem;">
|
||||
<div style="font-size: 2rem; margin-bottom: 1rem;"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div style="margin-bottom: 1rem; font-size: 1.125rem;">${t('connection_interrupted')}</div>
|
||||
<div style="background: var(--bg-tertiary); padding: 1.5rem; border-radius: 8px; max-width: 500px; text-align: left; margin-bottom: 1rem;">
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 0.5rem;">
|
||||
<strong>${t('possible_reasons')}:</strong>
|
||||
</p>
|
||||
<ul style="color: var(--text-muted); font-size: 0.8rem; margin-left: 1rem;">
|
||||
<li>${t('vnc_requires_password')}</li>
|
||||
<li>${t('vnc_add_password')}</li>
|
||||
<li>${t('vnc_vm_may_be_off')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<button onclick="openVNC()" class="btn btn-primary"><i class="fa-solid fa-rotate"></i> ${t('reconnect')}</button>
|
||||
<a href="https://${vncData.host}:8006" target="_blank" class="btn btn-secondary"><i class="fa-solid fa-globe"></i> ${t('open_proxmox')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
vncConnection.addEventListener('securityfailure', (e) => {
|
||||
vncContent.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<div style="font-size: 2rem; margin-bottom: 1rem;"><i class="fa-solid fa-lock"></i></div>
|
||||
<div class="text-danger">${t('vnc_security_error')}</div>
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem; margin-top: 0.5rem;">${e.detail.reason || t('unknown_error')}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
vncContent.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--text-secondary);"><i class="fa-solid fa-circle-xmark"></i> ${error.message}</div>`;
|
||||
}
|
||||
|
||||
@@ -2043,45 +2043,36 @@ async function openVNCConsole(serverId, vmid, type, node, vmName) {
|
||||
const vncData = await response.json();
|
||||
vncContent.innerHTML = '';
|
||||
|
||||
// Create WebSocket URL
|
||||
// Create WebSocket URL (passed directly to noVNC — it manages the WebSocket)
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
const resourceType = vncData.type === 'qemu' ? 'vm' : 'container';
|
||||
|
||||
const wsUrl = `${protocol}//${wsHost}/proxmox/ws/vnc/${serverId}/${vncData.node}/${resourceType}/${vmid}?port=${vncData.port}&vncticket=${encodeURIComponent(vncData.ticket)}&auth_ticket=${encodeURIComponent(vncData.auth_ticket)}&vnc_password=${encodeURIComponent(vncData.password || '')}`;
|
||||
const wsUrl = `${protocol}//${wsHost}/proxmox/ws/vnc/${serverId}/${vncData.node}/${resourceType}/${vmid}?port=${vncData.port}&vncticket=${encodeURIComponent(vncData.ticket)}&auth_ticket=${encodeURIComponent(vncData.auth_ticket)}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
const vncPassword = vncData.password || '';
|
||||
|
||||
ws.addEventListener('message', function onFirstMessage(event) {
|
||||
const vncPassword = event.data;
|
||||
ws.removeEventListener('message', onFirstMessage);
|
||||
|
||||
window.vncConnection = new RFBClass(vncContent, ws, {
|
||||
credentials: { password: vncPassword }
|
||||
});
|
||||
|
||||
window.vncConnection.scaleViewport = true;
|
||||
window.vncConnection.resizeSession = true;
|
||||
|
||||
window.vncConnection.addEventListener('connect', () => {
|
||||
setTimeout(() => {
|
||||
if (window.vncConnection && window.vncConnection._display) {
|
||||
window.vncConnection._display.autoscale(vncContent.clientWidth, vncContent.clientHeight);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
window.vncConnection.addEventListener('credentialsrequired', (e) => {
|
||||
window.vncConnection.sendCredentials({ password: vncPassword });
|
||||
});
|
||||
|
||||
window.vncConnection.addEventListener('disconnect', (e) => {
|
||||
vncContent.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-muted);">{{ t("connection_closed") }}</div>';
|
||||
});
|
||||
window.vncConnection = new RFBClass(vncContent, wsUrl, {
|
||||
credentials: { password: vncPassword }
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (err) => {
|
||||
vncContent.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--danger);">{{ t("connection_error") }}</div>`;
|
||||
window.vncConnection.scaleViewport = true;
|
||||
window.vncConnection.resizeSession = true;
|
||||
|
||||
window.vncConnection.addEventListener('connect', () => {
|
||||
setTimeout(() => {
|
||||
if (window.vncConnection && window.vncConnection._display) {
|
||||
window.vncConnection._display.autoscale(vncContent.clientWidth, vncContent.clientHeight);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
window.vncConnection.addEventListener('credentialsrequired', (e) => {
|
||||
window.vncConnection.sendCredentials({ password: vncPassword });
|
||||
});
|
||||
|
||||
window.vncConnection.addEventListener('disconnect', (e) => {
|
||||
vncContent.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-muted);">{{ t("connection_closed") }}</div>';
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user