Enhance eMMC provisioning dashboard: implement cloud-init template management with options to load, save, and delete templates. Update UI to support selecting Raspberry Pi OS variant for image building and improve user instructions for cloud-init image creation. Add new API endpoints for managing cloud-init templates and fetching the latest Raspberry Pi OS URLs.
This commit is contained in:
@@ -419,15 +419,30 @@
|
||||
<!-- 3b. Build cloud-init image from official Raspberry Pi OS -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Build cloud-init image</h2>
|
||||
<p class="backup-deploy-hint">Download the latest <strong>Raspberry Pi OS Lite (arm64)</strong> from the official repository and inject cloud-init NoCloud files so the image is ready for first-boot configuration.</p>
|
||||
<p id="buildRaspiosUrl" class="backups-mono" style="font-size:0.8rem; margin-bottom:0.5rem;"></p>
|
||||
<p class="backup-deploy-hint">Download the latest <strong>Raspberry Pi OS (arm64)</strong> from the official repository and inject cloud-init NoCloud files. The built image appears in Saved backups; you can then choose it and click <strong>Set as golden</strong> for deployment.</p>
|
||||
<div style="margin-bottom:0.5rem;">
|
||||
<label>Variant: </label>
|
||||
<select id="buildVariant">
|
||||
<option value="lite">Lite (no desktop)</option>
|
||||
<option value="full">Full (desktop)</option>
|
||||
</select>
|
||||
<span id="buildRaspiosUrl" class="backups-mono" style="font-size:0.8rem; margin-left:0.5rem;"></span>
|
||||
</div>
|
||||
<div style="margin-bottom:0.5rem;">
|
||||
<label><input type="checkbox" id="buildSetGolden" /> Set as golden image after build</label>
|
||||
<span class="backups-mono" style="font-size:0.8rem;"> (use for Deploy without clicking manually)</span>
|
||||
</div>
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download latest Raspberry Pi OS Lite & build cloud-init image</button>
|
||||
<button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download & build cloud-init image</button>
|
||||
</div>
|
||||
<div id="buildCloudInitStatus" class="backups-mono" style="font-size:0.85rem; min-height:1.5em; margin-bottom:0.5rem;"></div>
|
||||
<details style="margin-top:0.5rem;">
|
||||
<summary>Customize cloud-init (optional)</summary>
|
||||
<summary>Cloud-init templates & customize</summary>
|
||||
<div class="inner" style="margin-top:0.5rem;">
|
||||
<p><strong>Templates:</strong> <select id="buildTemplateSelect"><option value="">— Load a template —</option></select>
|
||||
<button type="button" id="buildTemplateLoad" class="btn btn-outline btn-sm">Load</button>
|
||||
<button type="button" id="buildTemplateSave" class="btn btn-outline btn-sm">Save current as template…</button></p>
|
||||
<ul id="buildTemplateList" class="backups-mono" style="font-size:0.85rem; list-style:none; padding:0;"></ul>
|
||||
<label>user-data (YAML)</label>
|
||||
<textarea id="buildUserData" rows="8" style="width:100%; font-family:monospace; font-size:0.85rem; margin-bottom:0.5rem;" placeholder="Leave empty to use default (remote bootstrap example)"></textarea>
|
||||
<label>meta-data (optional)</label>
|
||||
@@ -745,12 +760,74 @@
|
||||
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) { renderBackups(d.backups || []); }).catch(function() {});
|
||||
}
|
||||
|
||||
function getBuildVariant() {
|
||||
var sel = document.getElementById('buildVariant');
|
||||
return (sel && sel.value) ? sel.value : 'lite';
|
||||
}
|
||||
function fetchRaspiosUrl() {
|
||||
fetch('/api/raspios-latest-url').then(function(r) { return r.json(); }).then(function(d) {
|
||||
var variant = getBuildVariant();
|
||||
fetch('/api/raspios-latest-url?variant=' + encodeURIComponent(variant)).then(function(r) { return r.json(); }).then(function(d) {
|
||||
var el = document.getElementById('buildRaspiosUrl');
|
||||
if (el) el.textContent = d.ok && d.filename ? ('Latest: ' + d.filename) : (d.error || 'Could not resolve latest image URL');
|
||||
}).catch(function() {});
|
||||
}
|
||||
function fetchCloudInitTemplates() {
|
||||
fetch('/api/cloudinit-templates').then(function(r) { return r.json(); }).then(function(d) {
|
||||
var list = d.templates || [];
|
||||
var sel = document.getElementById('buildTemplateSelect');
|
||||
var listEl = document.getElementById('buildTemplateList');
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="">— Load a template —</option>';
|
||||
list.forEach(function(t) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
opt.textContent = t.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (listEl) {
|
||||
listEl.innerHTML = list.map(function(t) {
|
||||
return '<li><span>' + escapeHtml(t.name) + '</span> <button type="button" class="btn btn-outline btn-sm template-load-btn" data-id="' + escapeHtml(t.id) + '">Load</button> <button type="button" class="btn btn-outline btn-sm template-del-btn" data-id="' + escapeHtml(t.id) + '">Delete</button></li>';
|
||||
}).join('') || '<li>No templates saved.</li>';
|
||||
listEl.querySelectorAll('.template-load-btn').forEach(function(btn) {
|
||||
btn.onclick = function() { loadTemplate(btn.getAttribute('data-id')); };
|
||||
});
|
||||
listEl.querySelectorAll('.template-del-btn').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
if (!confirm('Delete this template?')) return;
|
||||
fetch('/api/cloudinit-templates/' + encodeURIComponent(btn.getAttribute('data-id')), { method: 'DELETE' })
|
||||
.then(function(r) { return r.json(); }).then(function(data) { if (data.ok) fetchCloudInitTemplates(); });
|
||||
};
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
function loadTemplate(id) {
|
||||
fetch('/api/cloudinit-templates/' + encodeURIComponent(id)).then(function(r) { return r.json(); }).then(function(t) {
|
||||
var ud = document.getElementById('buildUserData');
|
||||
var md = document.getElementById('buildMetaData');
|
||||
var nc = document.getElementById('buildNetworkConfig');
|
||||
if (ud) ud.value = t.user_data || '';
|
||||
if (md) md.value = t.meta_data || '';
|
||||
if (nc) nc.value = t.network_config || '';
|
||||
}).catch(function() {});
|
||||
}
|
||||
function saveTemplate() {
|
||||
var name = prompt('Template name');
|
||||
if (!name || !name.trim()) return;
|
||||
var ud = document.getElementById('buildUserData');
|
||||
var md = document.getElementById('buildMetaData');
|
||||
var nc = document.getElementById('buildNetworkConfig');
|
||||
fetch('/api/cloudinit-templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
user_data: ud ? ud.value : '',
|
||||
meta_data: md ? md.value : '',
|
||||
network_config: nc ? nc.value : ''
|
||||
})}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.ok) { fetchCloudInitTemplates(); alert('Saved as "' + name + '"'); }
|
||||
else alert(d.error || 'Failed');
|
||||
}).catch(function() { alert('Failed'); });
|
||||
}
|
||||
|
||||
function fetchBuildStatus() {
|
||||
fetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
|
||||
@@ -762,8 +839,9 @@
|
||||
if (d.phase === 'idle' && !d.message) {
|
||||
el.textContent = '';
|
||||
} else if (d.phase === 'done') {
|
||||
el.textContent = 'Done: ' + (d.output_name || '') + ' — refresh the backups list below.';
|
||||
el.textContent = 'Done: ' + (d.output_name || '') + ' — see Saved backups below. Set as golden to use for Deploy.';
|
||||
fetchBackups();
|
||||
fetchGoldenInfo();
|
||||
} else if (d.phase === 'error') {
|
||||
el.textContent = 'Error: ' + (d.error || d.message || 'Unknown');
|
||||
} else {
|
||||
@@ -776,18 +854,22 @@
|
||||
function startBuildCloudInit() {
|
||||
var btn = document.getElementById('buildCloudInitBtn');
|
||||
if (btn) btn.disabled = true;
|
||||
var body = {};
|
||||
var ud = document.getElementById('buildUserData');
|
||||
var md = document.getElementById('buildMetaData');
|
||||
var nc = document.getElementById('buildNetworkConfig');
|
||||
if (ud && ud.value.trim()) body.user_data = ud.value.trim();
|
||||
if (md && md.value.trim()) body.meta_data = md.value.trim();
|
||||
if (nc && nc.value.trim()) body.network_config = nc.value.trim();
|
||||
var setGolden = document.getElementById('buildSetGolden');
|
||||
var body = {
|
||||
variant: getBuildVariant(),
|
||||
set_as_golden_after: setGolden && setGolden.checked,
|
||||
user_data: (ud && ud.value.trim()) ? ud.value.trim() : undefined,
|
||||
meta_data: (md && md.value.trim()) ? md.value.trim() : undefined,
|
||||
network_config: (nc && nc.value.trim()) ? nc.value.trim() : undefined
|
||||
};
|
||||
fetch('/api/build-cloudinit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
document.getElementById('buildCloudInitStatus').textContent = 'Build started. Waiting…';
|
||||
document.getElementById('buildCloudInitStatus').textContent = 'Build started on host. Waiting…';
|
||||
setTimeout(fetchBuildStatus, 2000);
|
||||
} else {
|
||||
alert(data.error || 'Failed to start build');
|
||||
@@ -820,8 +902,15 @@
|
||||
fetchGoldenInfo();
|
||||
fetchRaspiosUrl();
|
||||
fetchBuildStatus();
|
||||
fetchCloudInitTemplates();
|
||||
var buildBtn = document.getElementById('buildCloudInitBtn');
|
||||
if (buildBtn) buildBtn.onclick = startBuildCloudInit;
|
||||
var variantSel = document.getElementById('buildVariant');
|
||||
if (variantSel) variantSel.onchange = fetchRaspiosUrl;
|
||||
var templateLoadBtn = document.getElementById('buildTemplateLoad');
|
||||
if (templateLoadBtn) templateLoadBtn.onclick = function() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) loadTemplate(s.value); };
|
||||
var templateSaveBtn = document.getElementById('buildTemplateSave');
|
||||
if (templateSaveBtn) templateSaveBtn.onclick = saveTemplate;
|
||||
setInterval(fetchStatus, 2000);
|
||||
setInterval(fetchLog, 4000);
|
||||
setInterval(fetchPending, 2000);
|
||||
|
||||
Reference in New Issue
Block a user