Add build cancellation feature to cloud-init process</message>
<message>Implement a new API endpoint for cancelling ongoing cloud-init builds, allowing users to request a build cancellation via the dashboard. Update the dashboard UI to include a cancel button that appears during the build process, enhancing user experience by providing control over long-running operations. Modify the build script to check for cancellation requests, ensuring that builds can be stopped gracefully. This feature improves usability and responsiveness in the cloud-init image building workflow.
This commit is contained in:
@@ -44,6 +44,7 @@ GOLDEN_IMAGE = Path(os.environ.get("CM4_GOLDEN_IMAGE", str(BASE_DIR / "golden.im
|
|||||||
NETWORK_DEVICES_FILE = Path(os.environ.get("CM4_NETWORK_DEVICES_FILE", str(BASE_DIR / "network_devices.json")))
|
NETWORK_DEVICES_FILE = Path(os.environ.get("CM4_NETWORK_DEVICES_FILE", str(BASE_DIR / "network_devices.json")))
|
||||||
BUILD_STATUS_FILE = Path(os.environ.get("CM4_BUILD_STATUS_FILE", str(BASE_DIR / "build_cloudinit_status.json")))
|
BUILD_STATUS_FILE = Path(os.environ.get("CM4_BUILD_STATUS_FILE", str(BASE_DIR / "build_cloudinit_status.json")))
|
||||||
BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR / "build_cloudinit_request.json")))
|
BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR / "build_cloudinit_request.json")))
|
||||||
|
BUILD_CANCEL_FILE = BUILD_REQUEST_FILE.parent / "build_cloudinit_cancel"
|
||||||
SHRINK_REQUEST_FILE = Path(os.environ.get("CM4_SHRINK_REQUEST_FILE", str(BASE_DIR / "shrink_request.json")))
|
SHRINK_REQUEST_FILE = Path(os.environ.get("CM4_SHRINK_REQUEST_FILE", str(BASE_DIR / "shrink_request.json")))
|
||||||
SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR / "shrink_status.json")))
|
SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR / "shrink_status.json")))
|
||||||
FIRST_BOOT_STATUS_FILE = Path(os.environ.get("CM4_FIRST_BOOT_STATUS_FILE", str(BASE_DIR / "first_boot_status.json")))
|
FIRST_BOOT_STATUS_FILE = Path(os.environ.get("CM4_FIRST_BOOT_STATUS_FILE", str(BASE_DIR / "first_boot_status.json")))
|
||||||
@@ -1616,6 +1617,18 @@ def api_build_cloudinit_status():
|
|||||||
return jsonify(_build_status_read())
|
return jsonify(_build_status_read())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/build-cloudinit-cancel", methods=["POST"])
|
||||||
|
@require_admin
|
||||||
|
def api_build_cloudinit_cancel():
|
||||||
|
"""Request cancellation of the current build (host script checks for cancel file)."""
|
||||||
|
try:
|
||||||
|
BUILD_CANCEL_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
BUILD_CANCEL_FILE.write_text("")
|
||||||
|
return jsonify({"ok": True, "message": "Cancel requested. Build will stop at next check."})
|
||||||
|
except OSError as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/build-cloudinit", methods=["POST"])
|
@app.route("/api/build-cloudinit", methods=["POST"])
|
||||||
@require_admin
|
@require_admin
|
||||||
def api_build_cloudinit():
|
def api_build_cloudinit():
|
||||||
|
|||||||
@@ -76,7 +76,10 @@
|
|||||||
<span class="mono" style="font-size:0.85rem; margin-left:0.25rem;">+ date suffix</span>
|
<span class="mono" style="font-size:0.85rem; margin-left:0.25rem;">+ date suffix</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:0.5rem;"><label><input type="checkbox" id="buildSetGolden" /> Set as golden after build</label></div>
|
<div style="margin-bottom:0.5rem;"><label><input type="checkbox" id="buildSetGolden" /> Set as golden after build</label></div>
|
||||||
<div style="margin-bottom:0.75rem;"><button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download & build</button></div>
|
<div style="margin-bottom:0.75rem;">
|
||||||
|
<button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download & build</button>
|
||||||
|
<button type="button" id="buildCloudInitCancelBtn" class="btn btn-outline" style="display:none; margin-left:0.5rem;">Cancel build</button>
|
||||||
|
</div>
|
||||||
<div id="buildCloudInitStatus" class="mono" style="min-height:1.2em;"></div>
|
<div id="buildCloudInitStatus" class="mono" style="min-height:1.2em;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,15 +139,27 @@
|
|||||||
authFetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
|
authFetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
|
||||||
var el = document.getElementById('buildCloudInitStatus');
|
var el = document.getElementById('buildCloudInitStatus');
|
||||||
var btn = document.getElementById('buildCloudInitBtn');
|
var btn = document.getElementById('buildCloudInitBtn');
|
||||||
|
var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0;
|
var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0;
|
||||||
if (btn) btn.disabled = busy;
|
if (btn) btn.disabled = busy;
|
||||||
|
if (cancelBtn) { cancelBtn.style.display = busy ? 'inline-block' : 'none'; cancelBtn.disabled = false; }
|
||||||
if (d.phase === 'idle' && !d.message) el.textContent = '';
|
if (d.phase === 'idle' && !d.message) el.textContent = '';
|
||||||
else if (d.phase === 'done') { el.textContent = 'Done: ' + (d.output_name || '') + ' — see Admin page Cloud-init images.'; }
|
else if (d.phase === 'done') { el.textContent = 'Done: ' + (d.output_name || '') + ' — see Admin page Cloud-init images.'; }
|
||||||
else if (d.phase === 'error') el.textContent = 'Error: ' + (d.error || '');
|
else if (d.phase === 'error') el.textContent = 'Error: ' + (d.error || '');
|
||||||
|
else if (d.phase === 'cancelled') el.textContent = 'Build cancelled.';
|
||||||
else el.textContent = (d.phase || '') + ': ' + (d.message || '');
|
else el.textContent = (d.phase || '') + ': ' + (d.message || '');
|
||||||
if (busy) setTimeout(fetchBuildStatus, 5000);
|
if (busy) setTimeout(fetchBuildStatus, 5000);
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
}
|
}
|
||||||
|
function cancelBuild() {
|
||||||
|
var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
|
if (cancelBtn) cancelBtn.disabled = true;
|
||||||
|
document.getElementById('buildCloudInitStatus').textContent = 'Cancelling…';
|
||||||
|
authFetch('/api/build-cloudinit-cancel', { method: 'POST', headers: {'Content-Type':'application/json'} }).then(function(r) { return r.json(); }).then(function(d) {
|
||||||
|
if (d.ok) setTimeout(fetchBuildStatus, 2000);
|
||||||
|
else { alert(d.error || 'Cancel failed'); if (cancelBtn) cancelBtn.disabled = false; }
|
||||||
|
}).catch(function() { if (cancelBtn) cancelBtn.disabled = false; });
|
||||||
|
}
|
||||||
function startBuild() {
|
function startBuild() {
|
||||||
var btn = document.getElementById('buildCloudInitBtn');
|
var btn = document.getElementById('buildCloudInitBtn');
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
@@ -211,6 +226,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('buildCloudInitBtn').onclick = startBuild;
|
document.getElementById('buildCloudInitBtn').onclick = startBuild;
|
||||||
|
var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
|
if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuild;
|
||||||
document.getElementById('buildVariant').onchange = function() {
|
document.getElementById('buildVariant').onchange = function() {
|
||||||
authFetch('/api/raspios-latest-url?variant=' + encodeURIComponent(document.getElementById('buildVariant').value)).then(function(r) { return r.json(); }).then(function(d) {
|
authFetch('/api/raspios-latest-url?variant=' + encodeURIComponent(document.getElementById('buildVariant').value)).then(function(r) { return r.json(); }).then(function(d) {
|
||||||
document.getElementById('buildRaspiosUrl').textContent = (d.ok && d.filename) ? d.filename : (d.error || '');
|
document.getElementById('buildRaspiosUrl').textContent = (d.ok && d.filename) ? d.filename : (d.error || '');
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
|
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
|
||||||
.progress-fill.indeterminate { width: 35%; animation: slide 1.2s ease-in-out infinite; }
|
.progress-fill.indeterminate { width: 35%; animation: slide 1.2s ease-in-out infinite; }
|
||||||
@keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
|
@keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
|
||||||
|
.firstboot-progress-wrap { margin-top: 0.6rem; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
||||||
|
.firstboot-progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent)); border-radius: 3px; transition: width 0.4s ease-out; }
|
||||||
|
.firstboot-progress-fill.animated { background: linear-gradient(90deg, var(--accent-dim), var(--accent) 50%, var(--accent-dim)); background-size: 200% 100%; animation: firstboot-shimmer 1.5s ease-in-out infinite; }
|
||||||
|
@keyframes firstboot-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
||||||
|
.firstboot-step-label { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 0.35rem; }
|
||||||
.device-item {
|
.device-item {
|
||||||
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
|
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
|
||||||
padding: 0.6rem 0.75rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
padding: 0.6rem 0.75rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||||
@@ -110,7 +115,11 @@
|
|||||||
|
|
||||||
<div id="firstBootCard" class="card" style="margin-bottom: 1rem; display:none;">
|
<div id="firstBootCard" class="card" style="margin-bottom: 1rem; display:none;">
|
||||||
<h2 class="card-title">First-boot progress</h2>
|
<h2 class="card-title">First-boot progress</h2>
|
||||||
<div id="firstBootStatus" class="status-row">
|
<p id="firstBootStepLabel" class="firstboot-step-label">Step 0 of 13</p>
|
||||||
|
<div id="firstBootProgressWrap" class="firstboot-progress-wrap" style="display:none;">
|
||||||
|
<div id="firstBootProgressFill" class="firstboot-progress-fill" style="width:0%;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="firstBootStatus" class="status-row" style="margin-top: 0.5rem;">
|
||||||
<span id="firstBootStep" class="status-pill idle"></span>
|
<span id="firstBootStep" class="status-pill idle"></span>
|
||||||
<span id="firstBootMsg" class="status-msg"></span>
|
<span id="firstBootMsg" class="status-msg"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,6 +248,7 @@
|
|||||||
function fmtSize(n) { if (n >= 1e9) return (n/1e9).toFixed(1)+' GB'; if (n >= 1e6) return (n/1e6).toFixed(1)+' MB'; return (n/1e3).toFixed(0)+' KB'; }
|
function fmtSize(n) { if (n >= 1e9) return (n/1e9).toFixed(1)+' GB'; if (n >= 1e6) return (n/1e6).toFixed(1)+' MB'; return (n/1e3).toFixed(0)+' KB'; }
|
||||||
function fmtDate(ts) { return new Date(ts*1000).toLocaleString(); }
|
function fmtDate(ts) { return new Date(ts*1000).toLocaleString(); }
|
||||||
function fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); }
|
function fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); }
|
||||||
|
const FIRST_BOOT_TOTAL_STEPS = 13;
|
||||||
function renderFirstBootStatus(data) {
|
function renderFirstBootStatus(data) {
|
||||||
const phase = data.phase || 'idle';
|
const phase = data.phase || 'idle';
|
||||||
const card = document.getElementById('firstBootCard');
|
const card = document.getElementById('firstBootCard');
|
||||||
@@ -246,8 +256,23 @@
|
|||||||
card.style.display = 'block';
|
card.style.display = 'block';
|
||||||
const stepEl = document.getElementById('firstBootStep');
|
const stepEl = document.getElementById('firstBootStep');
|
||||||
const msgEl = document.getElementById('firstBootMsg');
|
const msgEl = document.getElementById('firstBootMsg');
|
||||||
|
const stepLabelEl = document.getElementById('firstBootStepLabel');
|
||||||
|
const progressWrap = document.getElementById('firstBootProgressWrap');
|
||||||
|
const progressFill = document.getElementById('firstBootProgressFill');
|
||||||
const ipWrap = document.getElementById('firstBootIpWrap');
|
const ipWrap = document.getElementById('firstBootIpWrap');
|
||||||
const ipEl = document.getElementById('firstBootIp');
|
const ipEl = document.getElementById('firstBootIp');
|
||||||
|
var stepNum = parseInt(data.step, 10) || 0;
|
||||||
|
if (phase === 'done') stepNum = FIRST_BOOT_TOTAL_STEPS;
|
||||||
|
var progress = stepNum > 0 ? (stepNum / FIRST_BOOT_TOTAL_STEPS * 100) : 0;
|
||||||
|
var inProgress = (phase === 'started' || phase === 'running');
|
||||||
|
stepLabelEl.textContent = 'Step ' + stepNum + ' of ' + FIRST_BOOT_TOTAL_STEPS + (data.step_name ? ' · ' + data.step_name : '');
|
||||||
|
if (inProgress || phase === 'done' || phase === 'error') {
|
||||||
|
progressWrap.style.display = 'block';
|
||||||
|
progressFill.style.width = progress + '%';
|
||||||
|
if (inProgress) progressFill.classList.add('animated'); else progressFill.classList.remove('animated');
|
||||||
|
} else {
|
||||||
|
progressWrap.style.display = 'none';
|
||||||
|
}
|
||||||
stepEl.textContent = data.step ? 'Step ' + data.step : (phase === 'done' ? 'Done' : phase);
|
stepEl.textContent = data.step ? 'Step ' + data.step : (phase === 'done' ? 'Done' : phase);
|
||||||
stepEl.className = 'status-pill ' + (phase === 'done' ? 'done' : phase === 'error' ? 'error' : 'flashing');
|
stepEl.className = 'status-pill ' + (phase === 'done' ? 'done' : phase === 'error' ? 'error' : 'flashing');
|
||||||
msgEl.textContent = data.message || (phase === 'done' && data.ip ? 'First-boot finished. Device IP: ' + data.ip : '');
|
msgEl.textContent = data.message || (phase === 'done' && data.ip ? 'First-boot finished. Device IP: ' + data.ip : '');
|
||||||
|
|||||||
@@ -448,6 +448,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download & build cloud-init image</button>
|
<button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download & build cloud-init image</button>
|
||||||
|
<button type="button" id="buildCloudInitCancelBtn" class="btn btn-outline" style="display:none; margin-left:0.5rem;">Cancel build</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="buildCloudInitStatus" class="backups-mono" style="font-size:0.85rem; min-height:1.5em; margin-bottom:0.5rem;"></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;">
|
<details style="margin-top:0.5rem;">
|
||||||
@@ -893,9 +894,11 @@
|
|||||||
fetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
|
fetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
|
||||||
var el = document.getElementById('buildCloudInitStatus');
|
var el = document.getElementById('buildCloudInitStatus');
|
||||||
var btn = document.getElementById('buildCloudInitBtn');
|
var btn = document.getElementById('buildCloudInitBtn');
|
||||||
|
var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0;
|
var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0;
|
||||||
if (btn) btn.disabled = busy;
|
if (btn) btn.disabled = busy;
|
||||||
|
if (cancelBtn) { cancelBtn.style.display = busy ? 'inline-block' : 'none'; cancelBtn.disabled = false; }
|
||||||
if (d.phase === 'idle' && !d.message) {
|
if (d.phase === 'idle' && !d.message) {
|
||||||
el.textContent = '';
|
el.textContent = '';
|
||||||
} else if (d.phase === 'done') {
|
} else if (d.phase === 'done') {
|
||||||
@@ -904,12 +907,26 @@
|
|||||||
fetchGoldenInfo();
|
fetchGoldenInfo();
|
||||||
} else if (d.phase === 'error') {
|
} else if (d.phase === 'error') {
|
||||||
el.textContent = 'Error: ' + (d.error || d.message || 'Unknown');
|
el.textContent = 'Error: ' + (d.error || d.message || 'Unknown');
|
||||||
|
} else if (d.phase === 'cancelled') {
|
||||||
|
el.textContent = 'Build cancelled.';
|
||||||
} else {
|
} else {
|
||||||
el.textContent = (d.phase || '') + ': ' + (d.message || '');
|
el.textContent = (d.phase || '') + ': ' + (d.message || '');
|
||||||
}
|
}
|
||||||
if (busy) setTimeout(fetchBuildStatus, 5000);
|
if (busy) setTimeout(fetchBuildStatus, 5000);
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
}
|
}
|
||||||
|
function cancelBuildCloudInit() {
|
||||||
|
var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
|
if (cancelBtn) cancelBtn.disabled = true;
|
||||||
|
document.getElementById('buildCloudInitStatus').textContent = 'Cancelling…';
|
||||||
|
fetch('/api/build-cloudinit-cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) setTimeout(fetchBuildStatus, 2000);
|
||||||
|
else alert(d.error || 'Cancel request failed');
|
||||||
|
})
|
||||||
|
.catch(function() { if (cancelBtn) cancelBtn.disabled = false; });
|
||||||
|
}
|
||||||
|
|
||||||
function startBuildCloudInit() {
|
function startBuildCloudInit() {
|
||||||
var btn = document.getElementById('buildCloudInitBtn');
|
var btn = document.getElementById('buildCloudInitBtn');
|
||||||
@@ -993,6 +1010,8 @@
|
|||||||
fetchCloudInitTemplates();
|
fetchCloudInitTemplates();
|
||||||
var buildBtn = document.getElementById('buildCloudInitBtn');
|
var buildBtn = document.getElementById('buildCloudInitBtn');
|
||||||
if (buildBtn) buildBtn.onclick = startBuildCloudInit;
|
if (buildBtn) buildBtn.onclick = startBuildCloudInit;
|
||||||
|
var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn');
|
||||||
|
if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuildCloudInit;
|
||||||
var variantSel = document.getElementById('buildVariant');
|
var variantSel = document.getElementById('buildVariant');
|
||||||
if (variantSel) variantSel.onchange = fetchRaspiosUrl;
|
if (variantSel) variantSel.onchange = fetchRaspiosUrl;
|
||||||
var templateLoadBtn = document.getElementById('buildTemplateLoad');
|
var templateLoadBtn = document.getElementById('buildTemplateLoad');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ set -e
|
|||||||
PROV_DIR="${CM4_PROVISIONING_DIR:-/var/lib/cm4-provisioning}"
|
PROV_DIR="${CM4_PROVISIONING_DIR:-/var/lib/cm4-provisioning}"
|
||||||
REQUEST_FILE="$PROV_DIR/build_cloudinit_request.json"
|
REQUEST_FILE="$PROV_DIR/build_cloudinit_request.json"
|
||||||
STATUS_FILE="$PROV_DIR/build_cloudinit_status.json"
|
STATUS_FILE="$PROV_DIR/build_cloudinit_status.json"
|
||||||
|
CANCEL_FILE="$PROV_DIR/build_cloudinit_cancel"
|
||||||
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
||||||
BACKUPS_DIR="${BACKUPS_DIR:-$PROV_DIR/backups}"
|
BACKUPS_DIR="${BACKUPS_DIR:-$PROV_DIR/backups}"
|
||||||
CLOUDINIT_IMAGES_DIR="${CLOUDINIT_IMAGES_DIR:-$PROV_DIR/cloudinit-images}"
|
CLOUDINIT_IMAGES_DIR="${CLOUDINIT_IMAGES_DIR:-$PROV_DIR/cloudinit-images}"
|
||||||
@@ -25,7 +26,16 @@ write_status() {
|
|||||||
"$(date +%s)" > "$STATUS_FILE"
|
"$(date +%s)" > "$STATUS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_cancel() {
|
||||||
|
if [[ -f "$CANCEL_FILE" ]]; then
|
||||||
|
write_status "cancelled" "Build cancelled." "" ""
|
||||||
|
rm -f "$REQUEST_FILE" "$CANCEL_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
[[ -f "$REQUEST_FILE" ]] || { echo "No request file"; exit 0; }
|
[[ -f "$REQUEST_FILE" ]] || { echo "No request file"; exit 0; }
|
||||||
|
check_cancel
|
||||||
|
|
||||||
# Use temp dir on provisioning dir (not /tmp) so we have enough space for decompress (~3GB+)
|
# Use temp dir on provisioning dir (not /tmp) so we have enough space for decompress (~3GB+)
|
||||||
mkdir -p "$PROV_DIR"
|
mkdir -p "$PROV_DIR"
|
||||||
@@ -75,6 +85,7 @@ CURL_ERR="$TEMP_DIR/curl_err.txt"
|
|||||||
SHA256_FILE="$TEMP_DIR/expected.sha256"
|
SHA256_FILE="$TEMP_DIR/expected.sha256"
|
||||||
|
|
||||||
# Fetch official SHA256 checksum if available (Raspberry Pi OS publishes .img.xz.sha256 next to each image)
|
# Fetch official SHA256 checksum if available (Raspberry Pi OS publishes .img.xz.sha256 next to each image)
|
||||||
|
check_cancel
|
||||||
write_status "downloading" "Checking for existing image and checksum…" "" ""
|
write_status "downloading" "Checking for existing image and checksum…" "" ""
|
||||||
EXPECTED_HASH=""
|
EXPECTED_HASH=""
|
||||||
if curl -sfL -o "$SHA256_FILE" "$SHA256_URL" 2>/dev/null && [[ -s "$SHA256_FILE" ]]; then
|
if curl -sfL -o "$SHA256_FILE" "$SHA256_URL" 2>/dev/null && [[ -s "$SHA256_FILE" ]]; then
|
||||||
@@ -122,6 +133,7 @@ else
|
|||||||
cp "$XZ_FILE" "$CACHED"
|
cp "$XZ_FILE" "$CACHED"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
check_cancel
|
||||||
write_status "decompressing" "Decompressing image…" "" ""
|
write_status "decompressing" "Decompressing image…" "" ""
|
||||||
# Check we have a real xz file (not HTML error page)
|
# Check we have a real xz file (not HTML error page)
|
||||||
if ! command -v xz >/dev/null 2>&1; then
|
if ! command -v xz >/dev/null 2>&1; then
|
||||||
@@ -156,6 +168,7 @@ if ! xz -T 1 -d -k -f "$XZ_FILE" 2>"$XZ_ERR"; then
|
|||||||
fi
|
fi
|
||||||
[[ -f "$IMG_FILE" ]] || { write_status "error" "" "" "image.img not found after decompress"; exit 1; }
|
[[ -f "$IMG_FILE" ]] || { write_status "error" "" "" "image.img not found after decompress"; exit 1; }
|
||||||
|
|
||||||
|
check_cancel
|
||||||
write_status "injecting" "Mounting boot partition and injecting cloud-init…" "" ""
|
write_status "injecting" "Mounting boot partition and injecting cloud-init…" "" ""
|
||||||
LOOP=$(losetup -f --show -P "$IMG_FILE")
|
LOOP=$(losetup -f --show -P "$IMG_FILE")
|
||||||
boot_part="${LOOP}p1"
|
boot_part="${LOOP}p1"
|
||||||
@@ -171,6 +184,7 @@ cp "$TEMP_DIR/network-config" "$MNT/network-config"
|
|||||||
umount "$MNT"
|
umount "$MNT"
|
||||||
losetup -d "$LOOP"
|
losetup -d "$LOOP"
|
||||||
|
|
||||||
|
check_cancel
|
||||||
write_status "finalizing" "Copying image to cloud-init images…" "" ""
|
write_status "finalizing" "Copying image to cloud-init images…" "" ""
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
cp "$IMG_FILE" "$OUT_PATH"
|
cp "$IMG_FILE" "$OUT_PATH"
|
||||||
|
|||||||
Reference in New Issue
Block a user