vm-bench/frontend/templates/scp.html

319 lines
14 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="pve-panel" id="scp-form-panel">
<div class="pve-panel-header">
<span class="pve-panel-title">SCP Pull — Fetch File from Remote Server</span>
</div>
<div class="pve-panel-body">
<p class="pve-dim" style="margin-bottom:1rem;">
Pull a disk image directly from a customer server via SCP.
No laptop middle-hop — files land directly in the staging area.
<strong>Supports files of any size.</strong>
</p>
<form id="scp-form" onsubmit="startScpPull(event)">
<div class="pve-row">
<div class="pve-form-group">
<label>Remote Host</label>
<input type="text" name="scp_host" required class="pve-input"
placeholder="10.0.0.50 or server.example.com">
</div>
<div class="pve-form-group">
<label>SSH Port</label>
<input type="number" name="scp_port" class="pve-input" value="22" min="1" max="65535">
</div>
</div>
<div class="pve-row">
<div class="pve-form-group">
<label>Username</label>
<input type="text" name="scp_user" required class="pve-input" placeholder="root">
</div>
<div class="pve-form-group">
<label>Password</label>
<input type="password" name="scp_pass" required class="pve-input" placeholder="••••••••">
</div>
</div>
<div class="pve-form-group">
<label>Remote File Path *</label>
<input type="text" name="scp_path" required class="pve-input"
placeholder="/mnt/vmware/exchange-server.vmdk">
<span class="pve-hint">Full path to the disk image or archive on the remote server.</span>
</div>
<button type="submit" id="scp-start-btn" class="pve-btn pve-btn-primary">Start SCP Pull</button>
</form>
<div id="scp-status" class="pve-hidden" style="margin-top:1rem;">
<div id="scp-phase-label" style="margin-bottom:0.5rem;"></div>
<div class="pve-progress">
<div class="pve-progress-bar" id="scp-progress-fill" style="width:0%">0%</div>
</div>
<div id="scp-error"></div>
</div>
</div>
</div>
<div id="scp-analysis-section"></div>
<script>
const SESSION_ID = '{{ session_id }}';
async function startScpPull(e) {
e.preventDefault();
const btn = document.getElementById('scp-start-btn');
const status = document.getElementById('scp-status');
const analysis = document.getElementById('scp-analysis-section');
btn.disabled = true;
analysis.innerHTML = '';
status.classList.remove('pve-hidden');
document.getElementById('scp-error').textContent = '';
const formData = new FormData(e.target);
formData.append('session_id', SESSION_ID);
setScpPhase('Connecting to remote server...', 0, true);
try {
const resp = await fetch('/scp/start', { method: 'POST', body: formData });
const data = await resp.json();
if (data.phase === 'error') {
setScpPhase('SCP pull failed', 0, false);
showScpError(data.error);
btn.disabled = false; return;
}
const filename = data.filename;
const totalGb = data.total_size_gb;
setScpPhase('Pulling file...' + (totalGb ? ' (' + totalGb + ' GiB total)' : ''), 0, true);
for (;;) {
await new Promise(r => setTimeout(r, 2000));
try {
const pr = await fetch('/scp/progress/' + SESSION_ID + '/' + encodeURIComponent(filename));
const pdata = await pr.json();
if (pdata.phase === 'complete') {
setScpPhase('Pull complete (' + (pdata.file_size_gb || 0) + ' GiB)', 100, false);
await new Promise(r => setTimeout(r, 500));
setScpPhase('Step 2/2: Analysing source image...', 0, true);
var fd = new FormData();
fd.append('filename', filename);
fd.append('session_id', SESSION_ID);
fd.append('vmid', '0');
fd.append('vm_name', '');
var ar = await fetch('/session/analyze', { method: 'POST', body: fd });
var adata = await ar.json();
if (adata.phase === 'error') {
setScpPhase('Analysis failed', 0, false);
showScpError(adata.error);
btn.disabled = false; return;
}
var analysisId = adata.analysis_id;
if (!analysisId) {
setScpPhase('Analysis failed', 0, false);
showScpError('Backend returned invalid response.');
btn.disabled = false; return;
}
var astart = Date.now();
var pollErrs = 0;
for (;;) {
await new Promise(r => setTimeout(r, 2000));
var sr = await fetch('/session/analyze/status/' + analysisId);
var sdata = await sr.json();
var elapsed = Math.round((Date.now() - astart) / 1000);
setScpPhase('Analysing... ' + (sdata.message || '') + ' (' + elapsed + 's)', sdata.status === 'completed' ? 100 : 50, sdata.status !== 'completed');
if (sdata.status === 'completed') {
window.location.href = '/session/configure/' + analysisId + '?source_filename=' + encodeURIComponent(filename) + '&session_id=' + SESSION_ID;
btn.disabled = false; return;
}
if (sdata.status === 'failed') {
setScpPhase('Analysis failed', 0, false);
showScpError(sdata.error_details || 'Analysis failed');
btn.disabled = false; return;
}
if (sdata.error) {
pollErrs++;
if (pollErrs > 15) {
setScpPhase('Analysis failed', 0, false);
showScpError('Backend unavailable — analysis may need restart.');
btn.disabled = false; return;
}
} else {
pollErrs = 0;
}
}
}
if (pdata.phase === 'error') {
setScpPhase('SCP pull failed', 0, false);
showScpError(pdata.error); btn.disabled = false; return;
}
if (pdata.phase === 'pulling') {
const gb = (pdata.file_size_gb || 0).toFixed(1);
let label = 'Pulling file... ' + gb + ' GiB';
if (pdata.speed_mbps) label += ' (' + pdata.speed_mbps + ' MB/s)';
if (pdata.eta) label += ' — ' + pdata.eta;
setScpPhase(label, pdata.pct || 0, !pdata.pct);
}
} catch (_) {}
}
} catch (err) {
setScpPhase('SCP pull failed', 0, false);
showScpError('Network error: ' + err.message);
btn.disabled = false;
}
}
function setScpPhase(label, pct, spinner) {
const el = document.getElementById('scp-phase-label');
if (el) el.innerHTML = (spinner ? '<span class="pve-spinner"></span> ' : '') + label;
const fill = document.getElementById('scp-progress-fill');
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct > 0 ? pct + '%' : ''; }
}
function showScpError(msg) {
const el = document.getElementById('scp-error');
if (el) el.innerHTML += '<div class="pve-alert pve-alert-error" style="margin-top:0.5rem;">' + msg + '</div>';
}
function scpInitConfirmForm() {
const form = document.getElementById('confirm-form');
if (!form) return;
form.addEventListener('submit', scpSubmitJob);
const bootSel = form.querySelector('select[name="auto_detect_boot"]');
if (bootSel) {
bootSel.addEventListener('change', function() {
document.getElementById('boot-type-group').style.display =
this.value === 'false' ? 'block' : 'none';
});
}
const storageSel = document.getElementById('storage-select');
const storageCustom = document.getElementById('storage-custom');
if (storageSel && storageCustom) {
storageSel.addEventListener('change', function() {
if (this.value === '__custom__') {
storageCustom.classList.remove('pve-hidden');
storageCustom.name = 'target_storage';
this.name = '';
} else {
storageCustom.classList.add('pve-hidden');
storageCustom.name = 'target_storage_custom';
this.name = 'target_storage';
}
});
}
}
async function scpSubmitJob(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('#submit-btn');
btn.disabled = true;
const formData = new FormData(form);
formData.append('session_id', SESSION_ID);
try {
const resp = await fetch('/session/confirm', { method: 'POST', body: formData });
const html = await resp.text();
document.getElementById('scp-form-panel').style.display = 'none';
document.getElementById('scp-analysis-section').innerHTML = '';
document.getElementById('scp-analysis-section').innerHTML = html;
scpInitPolling();
} catch (err) {
btn.disabled = false;
}
}
var _scpPollTimer = null;
function scpInitPolling() {
const container = document.getElementById('polling-container');
if (!container) return;
const jobId = container.dataset.jobId;
const vmid = container.dataset.vmid;
const sourceFilename = container.dataset.sourceFilename;
const startTime = Date.now();
var completed = false;
document.getElementById('btn-clone').onclick = () => scpCloneVM(jobId, vmid);
document.getElementById('btn-cleanup').onclick = () => {
fetch('/session/cleanup/' + jobId, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ delete_staging_files: true, session_id: SESSION_ID }) });
window.location.href = '/';
};
document.getElementById('btn-copy').onclick = () => {
document.getElementById('copy-options').classList.remove('pve-hidden');
};
var prev = parseInt(vmid) || 21000;
document.getElementById('reuse-vmid').value = prev + 1;
document.getElementById('reuse-vmname').value = (sourceFilename.split('.')[0] || 'vm') + '-2';
document.getElementById('btn-copy-start').onclick = () => scpCopyVM(jobId, vmid, sourceFilename);
async function poll() {
try {
const resp = await fetch('/session/status/' + jobId);
const data = await resp.json();
const pct = data.progress_percentage || 0;
var fill = document.getElementById('pve-progress-bar');
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct + '%'; }
var elMsg = document.getElementById('status-message');
if (elMsg) elMsg.textContent = data.message || '';
document.getElementById('elapsed').textContent = 'Elapsed: ' + Math.round((Date.now() - startTime) / 1000) + 's';
var badge = document.getElementById('status-badge');
if (badge) {
if (data.status === 'completed') { badge.className = 'pve-badge pve-badge-completed'; badge.textContent = 'Completed'; }
else if (data.status === 'failed') { badge.className = 'pve-badge pve-badge-failed'; badge.textContent = 'Failed'; }
else { badge.className = 'pve-badge pve-badge-running'; badge.innerHTML = '<span class="pve-spinner"></span> ' + (data.status === 'queued' ? 'Queued' : 'Processing'); }
}
if (data.status === 'completed' || data.status === 'failed') {
completed = true;
if (data.status === 'completed') {
var rs = document.getElementById('reuse-section');
if (rs) { rs.classList.remove('pve-hidden'); rs.scrollIntoView({ behavior: 'smooth' }); }
}
return;
}
} catch (err) {}
if (!completed) _scpPollTimer = setTimeout(poll, 2000);
}
_scpPollTimer = setTimeout(poll, 3000);
}
async function scpCloneVM(jobId, sourceVmid) {
var newVmid = parseInt(document.getElementById('reuse-vmid').value) || 0;
var newName = document.getElementById('reuse-vmname').value.trim();
if (!newVmid || !newName) { alert('Please fill VM Name and ID.'); return; }
var status = document.getElementById('reuse-submit-status');
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Cloning VM...';
try {
var resp = await fetch('/session/clone', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_vmid: sourceVmid, target_vmid: newVmid, target_name: newName }) });
var data = await resp.json();
status.innerHTML = data.status === 'completed' ? '<div class="pve-alert pve-alert-success">VM ' + newVmid + ' cloned!</div>' : '<div class="pve-alert pve-alert-error">Clone failed: ' + (data.error_details || 'Unknown') + '</div>';
} catch (err) { status.innerHTML = '<div class="pve-alert pve-alert-error">Clone failed: ' + err.message + '</div>'; }
}
async function scpCopyVM(jobId, vmid, sourceFilename) {
var newVmid = parseInt(document.getElementById('reuse-vmid').value) || 0;
var newName = document.getElementById('reuse-vmname').value.trim();
if (!newVmid || !newName) { alert('Please fill VM Name and ID.'); return; }
var status = document.getElementById('copy-status');
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Submitting job...';
var payload = new FormData();
payload.append('vmid', newVmid); payload.append('vm_name', newName);
payload.append('source_filename', sourceFilename);
payload.append('disk_format', 'vmdk');
payload.append('cpu_cores', document.getElementById('copy-cores').value || '2');
payload.append('ram_mb', document.getElementById('copy-ram').value || '4096');
payload.append('target_storage', document.getElementById('copy-storage').value || 'local-lvm');
var diskGb = document.getElementById('copy-disk').value;
if (diskGb) payload.append('target_disk_size_gb', diskGb);
payload.append('auto_detect_boot', 'true');
payload.append('boot_type', 'legacy');
payload.append('session_id', SESSION_ID);
try {
var resp = await fetch('/session/confirm', { method: 'POST', body: payload });
document.getElementById('scp-analysis-section').innerHTML = await resp.text();
scpInitPolling();
} catch (err) { status.innerHTML = '<div class="pve-alert pve-alert-error">Failed: ' + err.message + '</div>'; }
}
</script>
{% endblock %}