Compare commits
4 Commits
59f8ebe61d
...
123fd8748e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123fd8748e | ||
|
|
7dd9f2b74f | ||
|
|
2baf6423a5 | ||
|
|
1501c17ab9 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gnss-bootstrap-20260223-215010.img.xz
|
||||
gnss-bootstrap-20260223-215010.img.xz.bak
|
||||
@@ -13,9 +13,10 @@ Revisions are tracked project-wide; see repo root **README.md** and `scripts/bum
|
||||
emmc-provisioning/
|
||||
├── README.md ← You are here
|
||||
├── docs/ Documentation
|
||||
│ ├── DEPLOY-NEW-PROXMOX.md Step-by-step: deploy to a new Proxmox instance
|
||||
│ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage
|
||||
│ ├── NETWORK-BOOT-LXC.md Network boot (PXE/dnsmasq) and LXC
|
||||
│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup
|
||||
│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup (reference)
|
||||
│ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference
|
||||
├── host/ Scripts that run on the provisioning host (Proxmox host)
|
||||
│ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd
|
||||
@@ -53,6 +54,7 @@ emmc-provisioning/
|
||||
## Quick start
|
||||
|
||||
1. **Read** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for setup and usage.
|
||||
2. **Proxmox:** Use [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) to deploy to a Proxmox host; see [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md).
|
||||
3. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide).
|
||||
4. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**.
|
||||
2. **Deploy to a new Proxmox:** Follow [docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md) for clear step-by-step instructions.
|
||||
3. **Proxmox reference:** [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) and [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md) for options, layout, and troubleshooting.
|
||||
4. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide).
|
||||
5. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Minimal bootstrap script for cloud-init first boot (test).
|
||||
set -e
|
||||
|
||||
# Ensure hostname resolves (avoids "sudo: unable to resolve host")
|
||||
H="$(hostname)"
|
||||
grep -q "127.0.1.1.*$H" /etc/hosts || echo "127.0.1.1 $H" >> /etc/hosts
|
||||
|
||||
echo "[$(date -Iseconds)] test completed" | tee -a /var/log/cloud-init-bootstrap.log
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
package_update: true
|
||||
package_upgrade: false
|
||||
|
||||
# Keep /etc/hosts in sync with hostname (from meta-data or set below)
|
||||
manage_etc_hosts: true
|
||||
|
||||
# DNS is managed by systemd-resolved; we do not overwrite /etc/resolv.conf
|
||||
manage_resolv_conf: false
|
||||
|
||||
packages:
|
||||
- curl
|
||||
|
||||
@@ -19,7 +25,76 @@ write_files:
|
||||
PasswordAuthentication yes
|
||||
PermitRootLogin no
|
||||
|
||||
# Push current DHCP DNS into systemd-resolved (for dhcpcd/dhclient when NM doesn't feed resolved).
|
||||
# With no args: discover DNS from lease or resolvectl and push to resolved for default IF.
|
||||
# NetworkManager feeds resolved automatically; this covers first boot and non-NM setups.
|
||||
- path: /usr/local/bin/update-resolv-from-dhcp.sh
|
||||
content: |
|
||||
#!/bin/sh
|
||||
# Push DHCP DNS to systemd-resolved so resolv.conf (stub) uses it.
|
||||
IF="${IFACE:-$(ip -o -4 route show to default 2>/dev/null | awk '{print $5}' | head -1)}"
|
||||
[ -z "$IF" ] && exit 0
|
||||
DNS=""
|
||||
if [ -s /run/systemd/resolve/resolv.conf ]; then
|
||||
DNS=$(grep -E '^nameserver\s+' /run/systemd/resolve/resolv.conf | awk '{print $2}' | tr '\n' ' ')
|
||||
fi
|
||||
if [ -z "$DNS" ]; then
|
||||
DNS=$(resolvectl dns "$IF" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ')
|
||||
fi
|
||||
if [ -z "$DNS" ]; then
|
||||
LEASE=$(ls /var/lib/dhcp/dhclient.*.leases 2>/dev/null | head -1)
|
||||
[ -n "$LEASE" ] && DNS=$(grep -oP 'option domain-name-servers \K[^;]+' "$LEASE" 2>/dev/null | tr ',' '\n' | tr -d ' ' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ')
|
||||
fi
|
||||
[ -n "$DNS" ] && resolvectl dns "$IF" $DNS
|
||||
permissions: '0755'
|
||||
|
||||
# dhclient: feed systemd-resolved on every lease acquire/renew (DHCP provides new_domain_name_servers)
|
||||
- path: /etc/dhcp/dhclient-exit-hooks.d/zzz-update-resolv-conf
|
||||
content: |
|
||||
#!/bin/sh
|
||||
# Run by dhclient on exit; push DHCP DNS into systemd-resolved.
|
||||
[ -z "$new_domain_name_servers" ] && exit 0
|
||||
[ -z "$interface" ] && exit 0
|
||||
resolvectl dns "$interface" $new_domain_name_servers
|
||||
permissions: '0755'
|
||||
|
||||
# NetworkManager: resolved is fed by NM by default; this only runs our script as fallback (e.g. if resolved started late).
|
||||
- path: /etc/NetworkManager/dispatcher.d/99-update-resolv-from-dhcp
|
||||
content: |
|
||||
#!/bin/sh
|
||||
[ "$2" = "up" ] || [ "$2" = "dhcp4-change" ] || exit 0
|
||||
export IFACE="$1"
|
||||
/usr/local/bin/update-resolv-from-dhcp.sh
|
||||
permissions: '0755'
|
||||
|
||||
# Tell NetworkManager to send DHCP DNS to systemd-resolved (so every DHCP update is applied).
|
||||
- path: /etc/NetworkManager/conf.d/99-use-resolved.conf
|
||||
content: |
|
||||
[main]
|
||||
dns=systemd-resolved
|
||||
rc-manager=unmanaged
|
||||
|
||||
# Fallback: push DHCP DNS to resolved once when network is up (e.g. dhcpcd-only or first boot).
|
||||
- path: /etc/systemd/system/update-resolv-from-dhcp.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Push DHCP DNS to systemd-resolved
|
||||
After=network-online.target systemd-resolved.service
|
||||
WantedBy=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/update-resolv-from-dhcp.sh
|
||||
RemainAfterExit=yes
|
||||
|
||||
runcmd:
|
||||
# Use systemd-resolved for DNS; /etc/resolv.conf -> stub so all lookups go through resolved (DHCP DNS applied by NM/hooks).
|
||||
- systemctl enable systemd-resolved.service
|
||||
- systemctl start systemd-resolved.service
|
||||
- rm -f /etc/resolv.conf && ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
|
||||
# Push current DHCP DNS into resolved once at first boot (in case NM hasn't applied yet).
|
||||
- /usr/local/bin/update-resolv-from-dhcp.sh
|
||||
- systemctl enable update-resolv-from-dhcp.service
|
||||
- systemctl enable ssh
|
||||
- systemctl start ssh
|
||||
# Download and run bootstrap script (edit URL to match your file server)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin · CM4 Provisioning</title>
|
||||
<title>Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -76,6 +76,23 @@
|
||||
<p id="goldenInfo" class="mono" style="font-size:0.9rem;">Loading…</p>
|
||||
</div>
|
||||
|
||||
<!-- Update boot (EEPROM) -->
|
||||
<div class="section" id="adminUpdateBootSection">
|
||||
<h2 class="section-title">Update boot (EEPROM)</h2>
|
||||
<p id="adminUpdateBootHint" class="mono" style="font-size:0.85rem; color:var(--text-muted);">When a device is connected in USB boot mode, it appears here. Use this to write EEPROM boot order (e.g. eMMC only) to the device.</p>
|
||||
<div id="adminUpdateBootDevice" style="display:none; margin-top:0.5rem;">
|
||||
<p class="mono" style="font-size:0.9rem; margin-bottom:0.5rem;">Device in USB boot mode</p>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
|
||||
<label>Boot order:</label>
|
||||
<select id="adminEepromPreset" class="eeprom-preset" title="Boot order">
|
||||
<option value="0x1">eMMC only</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="adminUpdateEepromBtn">Update EEPROM</button>
|
||||
</div>
|
||||
</div>
|
||||
<p id="adminUpdateBootNone" class="mono" style="font-size:0.85rem; color:var(--text-muted); display:none;">No device in USB boot mode. Connect a device with eMMC disable jumper and USB to host.</p>
|
||||
</div>
|
||||
|
||||
<!-- Backups -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
@@ -247,6 +264,47 @@
|
||||
authFetch('/api/admin/users', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username: username, password: password}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { document.getElementById('addUserForm').style.display = 'none'; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; fetchUsers(); } else alert(d.error); });
|
||||
};
|
||||
|
||||
function fetchPendingDevices() {
|
||||
authFetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){
|
||||
var devEl = document.getElementById('adminUpdateBootDevice');
|
||||
var noneEl = document.getElementById('adminUpdateBootNone');
|
||||
if (d.usb) {
|
||||
if (devEl) devEl.style.display = 'block';
|
||||
if (noneEl) noneEl.style.display = 'none';
|
||||
} else {
|
||||
if (devEl) devEl.style.display = 'none';
|
||||
if (noneEl) noneEl.style.display = 'block';
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
function fetchEepromPresets() {
|
||||
authFetch('/api/eeprom-presets').then(function(r){ return r.json(); }).then(function(d){
|
||||
var sel = document.getElementById('adminEepromPreset');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '';
|
||||
(d.presets || []).forEach(function(p){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id || p.value;
|
||||
opt.textContent = p.label || p.id || p.value;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}).catch(function(){});
|
||||
}
|
||||
fetchPendingDevices();
|
||||
fetchEepromPresets();
|
||||
var adminUpdateEepromBtn = document.getElementById('adminUpdateEepromBtn');
|
||||
if (adminUpdateEepromBtn) {
|
||||
adminUpdateEepromBtn.onclick = function(){
|
||||
var presetEl = document.getElementById('adminEepromPreset');
|
||||
var bootOrder = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
authFetch('/api/device-action', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ source: 'usb', action: 'eeprom_update', boot_order: bootOrder }) })
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){ if (d.ok) { fetchPendingDevices(); alert('Update EEPROM sent. The host will write to the device.'); } else alert(d.error || 'Failed'); })
|
||||
.catch(function(){ alert('Request failed'); });
|
||||
};
|
||||
}
|
||||
setInterval(fetchPendingDevices, 3000);
|
||||
|
||||
fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden();
|
||||
setInterval(fetchLogs, 30000);
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cloud-init build · Admin · CM4 Provisioning</title>
|
||||
<title>Cloud-init build · Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Deploy · CM4 Provisioning</title>
|
||||
<title>Deploy · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -96,7 +96,7 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="header">
|
||||
<h1>CM4 eMMC Provisioning</h1>
|
||||
<h1>GNSS Guard Provisioning</h1>
|
||||
<a href="/admin">Admin</a>
|
||||
</header>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<h2 class="card-title">Status</h2>
|
||||
<div id="status" class="status-row">
|
||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for device</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
|
||||
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||
</div>
|
||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Capture or deploy</h2>
|
||||
<h2 class="card-title">Deploy and Backup</h2>
|
||||
<p id="shrinkOptionWrap" style="display:none; margin-bottom: 0.5rem; font-size: 0.8rem;"><label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label></p>
|
||||
<div id="pendingDevices"></div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p>
|
||||
@@ -142,12 +142,12 @@
|
||||
<p id="goldenInfo" class="golden-info">Loading…</p>
|
||||
</div>
|
||||
|
||||
<details class="card" style="padding: 0;">
|
||||
<details class="card" style="padding: 0;" open>
|
||||
<summary>Recent log</summary>
|
||||
<div class="inner"><pre id="log" class="log-pre"></pre></div>
|
||||
</details>
|
||||
|
||||
<details class="card" style="padding: 0; margin-top: 1rem;">
|
||||
<details class="card" style="padding: 0; margin-top: 1rem;" open>
|
||||
<summary>DHCP leases</summary>
|
||||
<div class="inner">
|
||||
<p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p>
|
||||
@@ -162,7 +162,7 @@
|
||||
<div class="inner">
|
||||
<p class="help-sub">USB boot</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper).</li>
|
||||
<li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper).</li>
|
||||
<li><span class="num">2</span> Connect USB to host; choose <strong>Backup</strong> or <strong>Deploy</strong> above.</li>
|
||||
<li><span class="num">3</span> Remove jumper and power cycle when done.</li>
|
||||
</ol>
|
||||
@@ -178,7 +178,7 @@
|
||||
const phase = data.phase || 'idle';
|
||||
document.getElementById('statusPill').className = 'status-pill ' + phase;
|
||||
document.getElementById('statusPill').textContent = phaseLabels[phase] || phase;
|
||||
document.getElementById('statusMsg').textContent = data.message || '';
|
||||
document.getElementById('statusMsg').textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
|
||||
const err = document.getElementById('statusErr');
|
||||
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
|
||||
if (phase === 'done') scheduleDoneClear();
|
||||
@@ -204,7 +204,7 @@
|
||||
shrinkWrap.style.display = 'block';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-desc">USB device — choose Backup, Deploy, or Update EEPROM</div><div class="device-actions-row"><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button> <select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select> <button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>';
|
||||
el.innerHTML = '<div class="device-desc">USB device — choose Backup or Deploy</div><div class="device-actions-row"><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
} else shrinkWrap.style.display = 'none';
|
||||
noPending.style.display = hasAny ? 'none' : 'block';
|
||||
@@ -212,10 +212,6 @@
|
||||
btn.onclick = function() {
|
||||
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
|
||||
if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
|
||||
if (body.action === 'eeprom_update' && body.source === 'usb') {
|
||||
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
|
||||
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
}
|
||||
if (body.action === 'backup' && document.getElementById('shrinkAfterBackup').checked) body.shrink = true;
|
||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -267,7 +263,14 @@
|
||||
fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){});
|
||||
}
|
||||
function fetchPending() { fetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){ renderPending(d.usb || null, d.network || []); }).catch(function(){ renderPending(null, []); }); }
|
||||
function fetchLog() { fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){ document.getElementById('log').textContent = d.log || ''; }).catch(function(){}); }
|
||||
function fetchLog() {
|
||||
fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){
|
||||
var logEl = document.getElementById('log');
|
||||
if (!logEl) return;
|
||||
logEl.textContent = d.log || '';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}).catch(function(){});
|
||||
}
|
||||
function fetchGolden() {
|
||||
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
|
||||
const el = document.getElementById('goldenInfo');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CM4 eMMC Provisioning</title>
|
||||
<title>GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -359,8 +359,8 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="header">
|
||||
<h1>CM4 eMMC Provisioning</h1>
|
||||
<p>Deploy or backup reTerminal via USB boot mode</p>
|
||||
<h1>GNSS Guard Provisioning</h1>
|
||||
<p>Deploy or backup device via USB boot mode</p>
|
||||
</header>
|
||||
|
||||
<!-- 1. Current status -->
|
||||
@@ -368,12 +368,12 @@
|
||||
<h2 class="section-title">Current status</h2>
|
||||
<div id="status" class="status-row">
|
||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for device</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
|
||||
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||
</div>
|
||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||
<div id="statusGoldenHint" class="backup-deploy-hint" style="display:none; margin-top:0.75rem;">
|
||||
No golden image is required to <strong>capture</strong>. Connect a device in USB boot mode; when it appears under “Capture image or deploy”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
|
||||
No golden image is required to <strong>backup</strong>. Connect a device in USB boot mode; when it appears under “Deploy and Backup”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
|
||||
<button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button>
|
||||
</div>
|
||||
<div id="statusMeta" class="status-meta" style="display:none;"></div>
|
||||
@@ -384,8 +384,8 @@
|
||||
|
||||
<!-- 2. Capture (Backup) or Deploy -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Capture image or deploy</h2>
|
||||
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode. When the device appears below, click <strong>Backup</strong> to save its eMMC to a file. To write an image to a device, click <strong>Deploy</strong> (requires a golden image).</p>
|
||||
<h2 class="section-title">Deploy and Backup</h2>
|
||||
<p class="backup-deploy-hint">Connect a device in USB boot mode. When it appears below, click <strong>Backup</strong> to save its eMMC to a file, or <strong>Deploy</strong> to write the golden image to the device (requires a golden image set in Admin).</p>
|
||||
<p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;">
|
||||
<label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label> <span class="backups-mono" style="font-size:0.8rem;">(reduces size; requires PiShrink on host)</span>
|
||||
</p>
|
||||
@@ -398,7 +398,7 @@
|
||||
</span>
|
||||
<span>— USB boot mode</span>
|
||||
</div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect reTerminal in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect device in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Saved backups -->
|
||||
@@ -477,7 +477,7 @@
|
||||
<div class="inner">
|
||||
<p class="help-sub">USB boot mode</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
|
||||
<li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
|
||||
<li><span class="num">2</span> Connect <strong>USB slave</strong> to the host and power on. The device appears above; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li>
|
||||
<li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li>
|
||||
</ol>
|
||||
@@ -485,7 +485,7 @@
|
||||
</details>
|
||||
|
||||
<!-- 5. Recent log (collapsible) -->
|
||||
<details class="section" style="padding:0;">
|
||||
<details class="section" style="padding:0;" open>
|
||||
<summary>Recent log</summary>
|
||||
<div class="inner">
|
||||
<pre id="log" class="log-pre"></pre>
|
||||
@@ -516,7 +516,7 @@
|
||||
const phase = data.phase || 'idle';
|
||||
statusPill.className = 'status-pill ' + phase;
|
||||
statusPill.textContent = phaseLabels[phase] || phase;
|
||||
statusMsg.textContent = data.message || '';
|
||||
statusMsg.textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
|
||||
|
||||
if (data.error) {
|
||||
statusErr.textContent = data.error;
|
||||
@@ -566,7 +566,7 @@
|
||||
if (shrinkWrap) shrinkWrap.style.display = 'block';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup, Deploy, or Update EEPROM</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button><select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select><button type="button" class="btn btn-outline" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>';
|
||||
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
} else {
|
||||
if (shrinkWrap) shrinkWrap.style.display = 'none';
|
||||
@@ -583,10 +583,6 @@
|
||||
const mac = btn.getAttribute('data-mac');
|
||||
const body = { source: source, action: action };
|
||||
if (mac) body.mac = mac;
|
||||
if (action === 'eeprom_update' && source === 'usb') {
|
||||
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
|
||||
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
}
|
||||
const shrinkCb = document.getElementById('shrinkAfterBackup');
|
||||
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
|
||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
@@ -785,7 +781,12 @@
|
||||
fetch('/api/pending-devices').then(function(r) { return r.json(); }).then(function(d) { renderPending(d.usb || null, d.network || []); }).catch(function() { renderPending(null, []); });
|
||||
}
|
||||
function fetchLog() {
|
||||
fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) { document.getElementById('log').textContent = d.log || ''; }).catch(function() {});
|
||||
fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) {
|
||||
var logEl = document.getElementById('log');
|
||||
if (!logEl) return;
|
||||
logEl.textContent = d.log || '';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}).catch(function() {});
|
||||
}
|
||||
function fetchBackups() {
|
||||
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin login · CM4 Provisioning</title>
|
||||
<title>Admin login · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -28,7 +28,7 @@
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Admin login</h1>
|
||||
<p class="sub">CM4 eMMC provisioning dashboard</p>
|
||||
<p class="sub">GNSS Guard Provisioning admin</p>
|
||||
{% if error %}<p class="err">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="{{ url_for('login') }}">
|
||||
<label for="username">Username</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Portal files · Admin · CM4 Provisioning</title>
|
||||
<title>Portal files · Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
152
emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md
Normal file
152
emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Deploy CM4 eMMC Provisioning to a New Proxmox Instance
|
||||
|
||||
Step-by-step guide to deploy the provisioning service (host + LXC) on a **new** Proxmox server. For redeploy/update and troubleshooting, see [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites (before running the deploy script)
|
||||
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| **Proxmox host** | A Proxmox VE node (new or existing) where you want the service. |
|
||||
| **SSH as root** | You must be able to run `ssh root@YOUR_PROXMOX_HOST` with **key-based auth** (no password prompt). |
|
||||
| **Proxmox storage** | At least one active storage (e.g. `local` or `local-lvm`). Check on the host: `pvesm status`. |
|
||||
| **Host internet** (recommended) | Needed so the deploy script can download the Debian 12 LXC template (if missing), and install **usbboot** and **PiShrink** on the host. Without internet, deploy still runs but you must install usbboot and PiShrink manually later. |
|
||||
|
||||
**Optional (set before deploy):**
|
||||
|
||||
- `DEPLOY_ROOTFS_STORAGE=local-lvm` — Skip interactive storage choice when creating the LXC.
|
||||
- `DEPLOY_LXC_ROOT_PASSWORD=yourpassword` — Set LXC root password and enable SSH.
|
||||
- `DEPLOY_LXC_SSH_KEY=/path/to/pub` — Copy this key into the LXC (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub`).
|
||||
- `CM4_BACKUPS_HOST_PATH=/mnt/storage/cm4-backups` — Store backups on this host path (create the directory on the host if needed).
|
||||
- **Network (WAN/LAN):**
|
||||
`DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default),
|
||||
`DEPLOY_LXC_LAN_BRIDGE=vmbr1`, `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` — To add eth1 as provisioning LAN with a custom subnet.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Run the deploy script
|
||||
|
||||
From your **workstation** (where the repo is cloned), run:
|
||||
|
||||
```bash
|
||||
cd /path/to/reTerminal\ DM4
|
||||
|
||||
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
|
||||
Replace `YOUR_PROXMOX_HOST` with the Proxmox hostname or IP (e.g. `10.20.30.40`).
|
||||
|
||||
**Example with options:**
|
||||
|
||||
```bash
|
||||
DEPLOY_ROOTFS_STORAGE=local-lvm \
|
||||
DEPLOY_LXC_ROOT_PASSWORD='YourSecurePassword' \
|
||||
DEPLOY_LXC_LAN_BRIDGE=vmbr1 \
|
||||
DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 \
|
||||
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.20.30.40
|
||||
```
|
||||
|
||||
- On **first run**, the script will ask you to choose LXC rootfs storage (unless `DEPLOY_ROOTFS_STORAGE` is set). It then creates the LXC, installs host scripts, udev, systemd units, and the dashboard in the LXC.
|
||||
- The script prints the **LXC IP** at the end. Note it for the next steps (or get it with:
|
||||
`ssh root@YOUR_PROXMOX_HOST "pct exec \$(pct list -no-header -output vmid,name | awk '\''\$2==\"cm4-provisioning\"{print \$1}'\'') -- hostname -I"`).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Install usbboot on the host (if host had no internet during deploy)
|
||||
|
||||
USB flash/backup needs **rpiboot** on the Proxmox **host**. If the deploy log said usbboot install failed or was skipped:
|
||||
|
||||
**From your workstation:**
|
||||
|
||||
```bash
|
||||
scp emmc-provisioning/scripts/install-usbboot-on-host.sh root@YOUR_PROXMOX_HOST:/tmp/
|
||||
ssh root@YOUR_PROXMOX_HOST "bash /tmp/install-usbboot-on-host.sh"
|
||||
```
|
||||
|
||||
**Or on the Proxmox host** (if `/tmp/emmc-provisioning-deploy` is still there):
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST
|
||||
bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Add a golden image (required for Deploy)
|
||||
|
||||
To **write** an image to a device (Deploy), the host must have a **golden image** at `/var/lib/cm4-provisioning/golden.img`. Backup (read from device) works without it.
|
||||
|
||||
**Option A — From the dashboard**
|
||||
|
||||
1. Open **http://<LXC-IP>:5000** (use the LXC IP from the deploy output).
|
||||
2. Build a cloud-init image or upload/set an existing backup as golden (see dashboard Admin).
|
||||
|
||||
**Option B — Copy an image from your machine**
|
||||
|
||||
```bash
|
||||
scp /path/to/your-golden.img root@YOUR_PROXMOX_HOST:/var/lib/cm4-provisioning/golden.img
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: (Optional) SSH into the LXC
|
||||
|
||||
If you set `DEPLOY_LXC_ROOT_PASSWORD` or had a default SSH key, you can already run:
|
||||
|
||||
```bash
|
||||
ssh root@<LXC-IP>
|
||||
```
|
||||
|
||||
Otherwise, enable root SSH and add your key:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST
|
||||
# Or with password: ROOT_PASSWORD='YourPassword' ./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: (Optional) Network boot (DHCP + TFTP on eth1)
|
||||
|
||||
Only if you deployed with **`DEPLOY_LXC_LAN_BRIDGE`** (and optionally `DEPLOY_LXC_LAN_SUBNET`) and want to offer network boot to devices on that LAN:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
See [NETWORK-BOOT-LXC.md](NETWORK-BOOT-LXC.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: (Optional) Install PiShrink on the host
|
||||
|
||||
If the deploy log said PiShrink install failed (e.g. no internet), and you want **Shrink/Compress** in the dashboard to work:
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh"
|
||||
```
|
||||
|
||||
Or from your machine (stream the script): use the same pattern as in [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md) for `install-pishrink-on-host.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Summary checklist
|
||||
|
||||
| Step | Action | Required? |
|
||||
|------|--------|------------|
|
||||
| 1 | Run `deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST` | **Yes** |
|
||||
| 2 | Install usbboot on host (if deploy couldn’t) | For USB flash/backup |
|
||||
| 3 | Add `golden.img` for Deploy | For Deploy only |
|
||||
| 4 | SSH to LXC (or use setup-lxc-ssh.sh) | Optional |
|
||||
| 5 | Run setup-network-boot-on-lxc.sh (if using eth1 LAN) | Optional |
|
||||
| 6 | Install PiShrink on host (if deploy couldn’t) | For Shrink/Compress |
|
||||
|
||||
**After deployment:**
|
||||
|
||||
- **Dashboard:** http://<LXC-IP>:5000
|
||||
- **Golden image path (host and LXC):** `/var/lib/cm4-provisioning/golden.img`
|
||||
- **Disable auto-flash:** `ssh root@YOUR_PROXMOX_HOST "rm /etc/cm4-provisioning/enabled"`
|
||||
- **Enable again:** `ssh root@YOUR_PROXMOX_HOST "touch /etc/cm4-provisioning/enabled"`
|
||||
|
||||
Full reference: [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
|
||||
@@ -1,4 +1,4 @@
|
||||
# How to edit cloud-init files on the device before capturing the image
|
||||
# How to edit cloud-init files on the device or on an image file
|
||||
|
||||
The cloud-init **NoCloud** files live on the **boot partition**. On the running device they are at:
|
||||
|
||||
@@ -77,6 +77,34 @@ They are owned by **root** and need **sudo** to edit.
|
||||
|
||||
---
|
||||
|
||||
## Method 3: Edit on an image file (.img or .img.xz)
|
||||
|
||||
To edit cloud-init **inside a disk image** (e.g. `gnss-bootstrap-20260223-215010.img.xz`) without booting the device:
|
||||
|
||||
1. **Run the helper script** (requires `sudo` for loop device and mount):
|
||||
|
||||
```bash
|
||||
cd /path/to/reTerminal-DM4
|
||||
./emmc-provisioning/scripts/edit-cloudinit-on-image.sh gnss-bootstrap-20260223-215010.img.xz
|
||||
```
|
||||
|
||||
2. The script will:
|
||||
- Decompress the `.img.xz` (if needed; allow ~3GB free space and a minute or two)
|
||||
- Attach the image with `losetup` and mount the **boot** (FAT32) partition
|
||||
- Open your `$EDITOR` (or `nano`) on `user-data`, `meta-data`, and `network-config`
|
||||
- After you save and exit, unmount and recompress, **overwriting** the original `.img.xz`
|
||||
|
||||
3. **Back up the image** before running if you want to keep the original:
|
||||
```bash
|
||||
cp gnss-bootstrap-20260223-215010.img.xz gnss-bootstrap-20260223-215010.img.xz.bak
|
||||
```
|
||||
|
||||
4. **Options:**
|
||||
- `--no-recompress` — Leave the image decompressed after editing (do not overwrite the `.img.xz`).
|
||||
- `--replace-with-repo` — Copy `user-data`, `meta-data`, and `network-config` from `emmc-provisioning/cloud-init/` into the image before opening the editor (useful to start from repo templates).
|
||||
|
||||
---
|
||||
|
||||
## What to edit (typical)
|
||||
|
||||
- **meta-data**
|
||||
|
||||
@@ -48,7 +48,13 @@ The script will:
|
||||
|
||||
## Proxmox: adding eth1 to the LXC
|
||||
|
||||
If you create the container by hand or want a second interface:
|
||||
Use the deploy script with **`DEPLOY_LXC_LAN_BRIDGE`** and **`DEPLOY_LXC_LAN_SUBNET`** so the LXC is created with eth1 (LAN) from the start:
|
||||
|
||||
```bash
|
||||
DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
|
||||
Or add a second interface to an existing container by hand:
|
||||
|
||||
1. On the **Proxmox host**, add a second network device to the container, e.g.:
|
||||
```bash
|
||||
@@ -65,6 +71,22 @@ If you create the container by hand or want a second interface:
|
||||
|
||||
Your current LXC already has eth0 (10.130.60.141) and eth1 (10.20.50.1); the setup script only adds DHCP, TFTP, and NAT.
|
||||
|
||||
### Changing the LAN subnet
|
||||
|
||||
When you deploy with **`DEPLOY_LXC_LAN_SUBNET`** (e.g. `10.100.1.1/24`), the deploy script writes **`/opt/cm4-provisioning/lan-subnet.conf`** inside the LXC with `LAN_GW`, `LAN_CIDR`, and the DHCP range. All LXC services use this file:
|
||||
|
||||
- **dnsmasq** (DHCP range and, via the toggle script, TFTP next-server)
|
||||
- **nftables/iptables** (NAT source subnet)
|
||||
- **toggle-network-boot-dhcp.sh** (option 66/67 next-server)
|
||||
|
||||
So changing `DEPLOY_LXC_LAN_SUBNET` and **re-running the deploy script** updates `lan-subnet.conf`. To apply the new subnet to dnsmasq and NAT, **re-run the setup script** after redeploying:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
Then run **toggle enable** again if you use network boot: `ssh root@<LXC-IP> /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable`
|
||||
|
||||
## After setup: reTerminal network boot
|
||||
|
||||
1. Set the reTerminal **boot order** to try eMMC first, then network (e.g. `BOOT_ORDER=0xf21`): use the dashboard **Update EEPROM** when the device is connected via USB boot, or set manually (usbboot recovery / `rpi-eeprom-config` on device). Not set by first-boot.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The auto-flash **runs on the Proxmox host** (where the USB device appears). The **LXC** holds the dashboard and shares the **golden image** directory with the host.
|
||||
|
||||
**New deployment:** For a clear step-by-step guide to deploy on a **new** Proxmox instance, see **[DEPLOY-NEW-PROXMOX.md](DEPLOY-NEW-PROXMOX.md)**.
|
||||
|
||||
## One-command deploy
|
||||
|
||||
From your repo, a single run deploys **all** host and LXC files (scripts, systemd units, udev, dashboard):
|
||||
@@ -12,6 +14,8 @@ From your repo, a single run deploys **all** host and LXC files (scripts, system
|
||||
|
||||
Optional env: `CM4_BACKUPS_HOST_PATH=/path`, `DEPLOY_ROOTFS_STORAGE=local-lvm`, `DEPLOY_LXC_ROOT_PASSWORD=secret` (set root password in LXC and enable SSH), `DEPLOY_LXC_SSH_KEY=/path/to/pub` (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub` — copied to LXC root so you can `ssh root@<LXC-IP>`).
|
||||
|
||||
**Network (WAN / LAN):** `DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default or e.g. `192.168.1.10/24`), `DEPLOY_LXC_LAN_BRIDGE=vmbr1` (if set, add eth1 as LAN), `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` (LXC IP on LAN; default `10.20.50.1/24` when LAN bridge is set). Example: `DEPLOY_LXC_WAN_BRIDGE=vmbr0 DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./scripts/deploy-to-proxmox.sh root@host`.
|
||||
|
||||
The script **finds the container by hostname `cm4-provisioning`** (any existing ID). If none exists, it **creates a new LXC with the next available ID**. So you can redeploy repeatedly without assuming a fixed ID like 201.
|
||||
|
||||
## What is deployed
|
||||
@@ -210,7 +214,7 @@ Or copy `scripts/monitor-from-host.sh` to the host and run `./monitor-from-host.
|
||||
|
||||
4. **"No 'bootcode' files found in mass-storage-gadget64"** – Usually because `bootfiles.bin` is a **broken symlink** (e.g. `-> ../firmware/bootfiles.bin`) and that target doesn’t exist. **Fix on host:** run `scripts/fix-gadget-bootcode-on-host.sh` on the host (it removes the symlink and extracts `bootcode4.bin` from the installed rpiboot binary). From your machine: `ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh`. **Alternative:** repopulate the gadget dir with `./scripts/populate-gadget-on-host.sh root@10.130.60.224`, or full reinstall with `./scripts/build-and-deploy-usbboot-to-host.sh root@10.130.60.224`. Then verify: `ls -la /opt/usbboot/mass-storage-gadget64/` (should list a real `bootcode4.bin` or `bootfiles.bin`, plus `boot.img`, `config.txt`).
|
||||
|
||||
4. **Clear stuck error in portal** – If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for reTerminal in boot mode or network.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device.
|
||||
4. **Clear stuck error in portal** – If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for Device in USB boot mode.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device.
|
||||
|
||||
5. **"PiShrink not installed" when clicking Shrink/Compress** – Shrink and Compress run **on the host**, not in the LXC. Install PiShrink on the host: `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh`. Ensure deploy has been run so the host has `run-shrink-on-host.sh` and `cm4-shrink.path` enabled (`systemctl status cm4-shrink.path`).
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
fi
|
||||
log "Backup complete: $BACKUPS_DIR/$final_name"
|
||||
write_status "done" "Backup complete: $final_name" "100"
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "Backup failed" "null" "dd failed"
|
||||
fi
|
||||
@@ -230,7 +230,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
|
||||
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
|
||||
# Auto-reset status to idle after 90s so dashboard does not stay on this message (dashboard also auto-clears after 60s when open)
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "Flash failed" "null" "dd failed"
|
||||
fi
|
||||
@@ -265,7 +265,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
log "EEPROM update written. Remove eMMC disable jumper and power cycle to apply."
|
||||
write_status "done" "EEPROM update written to boot partition. Remove eMMC disable jumper and power cycle the reTerminal to apply." "100"
|
||||
rm -f "$EEPROM_UPD" "$EEPROM_SIG"
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "EEPROM update failed" "null" "Failed to copy pieeprom.upd/sig"
|
||||
fi
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
Config files for the **provisioning LXC** when using **eth1** as a provisioning LAN (DHCP + TFTP for network boot, NAT for internet).
|
||||
|
||||
**LAN subnet:** When you deploy with `DEPLOY_LXC_LAN_SUBNET` (e.g. `10.100.1.1/24`), the deploy script writes `/opt/cm4-provisioning/lan-subnet.conf` inside the LXC with `LAN_GW`, `LAN_CIDR`, and `DHCP_RANGE_START`/`DHCP_RANGE_END`. The setup script and toggle script read this file so dnsmasq, NAT, and PXE options all use the same subnet. If the file is missing, defaults are `10.20.50.1/24` and `10.20.50.100`–`10.20.50.200`.
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| **dnsmasq-network-boot.conf** | dnsmasq: DHCP + TFTP on eth1 only. Copied to `/etc/dnsmasq.d/` by `scripts/setup-network-boot-on-lxc.sh`. |
|
||||
| **nft-nat-lan.conf** | nftables NAT so 10.20.50.0/24 uses eth0 for internet. Applied by the setup script to `/etc/nftables.d/nat-lan.conf`. |
|
||||
| **dnsmasq-network-boot.conf** | Template: dnsmasq DHCP + TFTP on eth1. Setup script writes `/etc/dnsmasq.d/network-boot.conf` using values from `lan-subnet.conf`. |
|
||||
| **nft-nat-lan.conf** | Template: nftables NAT for LAN→WAN. Setup script writes `/etc/nftables.d/nat-lan.conf` using `LAN_CIDR` from `lan-subnet.conf`. |
|
||||
|
||||
Setup is done by running (from your machine):
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PXE/network-boot DHCP options (option 66 = next-server, 67 = boot file).
|
||||
# When this file is present, dnsmasq advertises network boot; when removed, devices get DHCP only and boot from local storage.
|
||||
# Toggle with: /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable|disable
|
||||
# Template; toggle script writes the real next-server from /opt/cm4-provisioning/lan-subnet.conf (LAN_GW).
|
||||
dhcp-option=66,10.20.50.1
|
||||
dhcp-option=67,start4cd.elf
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# dnsmasq: DHCP + TFTP on eth1 only (provisioning LAN).
|
||||
# Install to /etc/dnsmasq.d/network-boot.conf on the LXC.
|
||||
# Restrict to eth1 so we don't interfere with host/other DHCP.
|
||||
# When using setup-network-boot-on-lxc.sh, the actual subnet and DHCP range
|
||||
# come from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh).
|
||||
|
||||
# Listen only on eth1 (provisioning LAN)
|
||||
interface=eth1
|
||||
bind-interfaces
|
||||
|
||||
# DHCP range for devices on eth1 (adjust if you use a different subnet)
|
||||
# DHCP range for devices on eth1 (template; setup script uses lan-subnet.conf)
|
||||
dhcp-range=10.20.50.100,10.20.50.200,12h
|
||||
|
||||
# TFTP for Raspberry Pi / CM4 network boot
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# nftables: NAT for LAN (eth1) so clients use WAN (eth0) for internet.
|
||||
# Load with: nft -f /etc/nftables.d/nat-lan.conf
|
||||
# Or use the inline rules in setup-network-boot-on-lxc.sh (no separate file dependency).
|
||||
# When using setup-network-boot-on-lxc.sh, the subnet is taken from /opt/cm4-provisioning/lan-subnet.conf (LAN_CIDR).
|
||||
|
||||
table ip nat {
|
||||
chain postrouting {
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
# When disabled, TFTP is stopped and no boot server is advertised; DHCP still runs.
|
||||
# Usage: toggle-network-boot-dhcp.sh enable | disable | status
|
||||
# Run as root. Install to /opt/cm4-provisioning/toggle-network-boot-dhcp.sh
|
||||
# LAN gateway for TFTP/next-server is read from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh).
|
||||
|
||||
set -e
|
||||
PXE_CONF="/etc/dnsmasq.d/network-boot-pxe.conf"
|
||||
MAIN_CONF="/etc/dnsmasq.d/network-boot.conf"
|
||||
|
||||
LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf"
|
||||
if [[ -f "$LAN_CONF" ]]; then
|
||||
source "$LAN_CONF"
|
||||
else
|
||||
LAN_GW="10.20.50.1"
|
||||
fi
|
||||
|
||||
# Remove enable-tftp / tftp-root from main config if present (legacy; these belong in PXE conf)
|
||||
cleanup_main_conf() {
|
||||
if [ -f "$MAIN_CONF" ] && grep -q 'enable-tftp\|tftp-root' "$MAIN_CONF" 2>/dev/null; then
|
||||
@@ -18,19 +26,19 @@ cleanup_main_conf() {
|
||||
case "${1:-}" in
|
||||
enable)
|
||||
cleanup_main_conf
|
||||
cat > "$PXE_CONF" << 'EOF'
|
||||
cat > "$PXE_CONF" << EOF
|
||||
# PXE/network boot ENABLED - managed by toggle-network-boot-dhcp.sh
|
||||
# TFTP server (only active when network boot is enabled)
|
||||
enable-tftp
|
||||
tftp-root=/srv/tftpboot
|
||||
# BOOTP fields (siaddr = TFTP server, filename = boot file)
|
||||
dhcp-boot=start4cd.elf,,10.20.50.1
|
||||
dhcp-boot=start4cd.elf,,${LAN_GW}
|
||||
# DHCP options 66/67 (some PXE clients prefer these)
|
||||
dhcp-option=66,10.20.50.1
|
||||
dhcp-option=66,${LAN_GW}
|
||||
dhcp-option=67,start4cd.elf
|
||||
EOF
|
||||
systemctl restart dnsmasq 2>/dev/null || service dnsmasq restart 2>/dev/null || true
|
||||
echo "Network boot enabled."
|
||||
echo "Network boot enabled (TFTP next-server: $LAN_GW)."
|
||||
;;
|
||||
disable)
|
||||
cleanup_main_conf
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
# DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH
|
||||
# DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub)
|
||||
# DEPLOY_LOG=1 — also log to deploy-YYYYMMDD-HHMMSS.log
|
||||
# DEPLOY_LXC_WAN_BRIDGE=vmbr0 — Proxmox bridge for WAN (eth0); default vmbr0
|
||||
# DEPLOY_LXC_WAN_IP=dhcp — WAN address: dhcp (default) or static e.g. 192.168.1.10/24
|
||||
# DEPLOY_LXC_LAN_BRIDGE=vmbr1 — If set, add eth1 as LAN on this bridge (e.g. provisioning / network-boot)
|
||||
# DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 — LXC IP on LAN (gateway); used only if DEPLOY_LXC_LAN_BRIDGE is set; default 10.20.50.1/24
|
||||
#
|
||||
# Legacy: DEPLOY_LXC_NET1="name=eth1,bridge=vmbr1,ip=10.20.50.1/24" still works; overridden by DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET if both are set.
|
||||
#
|
||||
# Requires: ssh key access to root@<host>. For full install (usbboot, PiShrink), host needs internet.
|
||||
|
||||
@@ -144,8 +150,8 @@ rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git'
|
||||
|
||||
log "[4/5] Running remote install (host + LXC) ..."
|
||||
|
||||
# Pass optional LXC SSH vars (base64) and selected storage
|
||||
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE'
|
||||
# Pass optional LXC SSH vars (base64), selected storage, and network (WAN/LAN bridge + subnet)
|
||||
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}' DEPLOY_LXC_WAN_BRIDGE='${DEPLOY_LXC_WAN_BRIDGE:-}' DEPLOY_LXC_WAN_IP='${DEPLOY_LXC_WAN_IP:-}' DEPLOY_LXC_LAN_BRIDGE='${DEPLOY_LXC_LAN_BRIDGE:-}' DEPLOY_LXC_LAN_SUBNET='${DEPLOY_LXC_LAN_SUBNET:-}' DEPLOY_LXC_NET1='${DEPLOY_LXC_NET1:-}'" bash -s << 'REMOTE'
|
||||
set -e
|
||||
DEPLOY=/tmp/emmc-provisioning-deploy
|
||||
ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}"
|
||||
@@ -185,14 +191,24 @@ else
|
||||
fi
|
||||
[[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; }
|
||||
TMPL_NAME=$(basename "$DEBIAN12_TMPL")
|
||||
# Optional: add eth1 for network-boot LAN (DHCP+TFTP). Set DEPLOY_LXC_NET1 e.g. "name=eth1,bridge=vmbr1,ip=10.20.50.1/24"
|
||||
# WAN (eth0): bridge and IP from env; default vmbr0 + dhcp
|
||||
WAN_BRIDGE="${DEPLOY_LXC_WAN_BRIDGE:-vmbr0}"
|
||||
WAN_IP="${DEPLOY_LXC_WAN_IP:-dhcp}"
|
||||
# LAN (eth1): optional; use DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET, or legacy DEPLOY_LXC_NET1
|
||||
NET1_OPT=""
|
||||
if [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then
|
||||
if [[ -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]]; then
|
||||
LAN_SUBNET="${DEPLOY_LXC_LAN_SUBNET:-10.20.50.1/24}"
|
||||
NET1_OPT="--net1 name=eth1,bridge=${DEPLOY_LXC_LAN_BRIDGE},ip=${LAN_SUBNET}"
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 LAN bridge=$DEPLOY_LXC_LAN_BRIDGE ip=$LAN_SUBNET"
|
||||
elif [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then
|
||||
NET1_OPT="--net1 $DEPLOY_LXC_NET1"
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 from DEPLOY_LXC_NET1"
|
||||
else
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP (no LAN interface)"
|
||||
fi
|
||||
pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \
|
||||
--hostname "$LXC_HOSTNAME" --memory 1024 --swap 0 --cores 1 \
|
||||
--rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge=vmbr0,ip=dhcp $NET1_OPT \
|
||||
--rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge="$WAN_BRIDGE",ip="$WAN_IP" $NET1_OPT \
|
||||
--unprivileged 0 --features nesting=1 -tag cm4-provisioning
|
||||
mkdir -p /var/lib/cm4-provisioning
|
||||
pct set "$CTID" -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning
|
||||
@@ -302,6 +318,24 @@ fi
|
||||
log "Starting LXC $CTID if stopped ..."
|
||||
pct start "$CTID" 2>/dev/null || true
|
||||
|
||||
# --- LXC: write lan-subnet.conf when LAN bridge/subnet is set (so dnsmasq/NAT/toggle use same subnet) ---
|
||||
LAN_SUBNET_FOR_CONF="${DEPLOY_LXC_LAN_SUBNET:-}"
|
||||
[[ -z "$LAN_SUBNET_FOR_CONF" && -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]] && LAN_SUBNET_FOR_CONF="10.20.50.1/24"
|
||||
if [[ -n "$LAN_SUBNET_FOR_CONF" ]]; then
|
||||
if [[ "$LAN_SUBNET_FOR_CONF" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$ ]]; then
|
||||
LAN_GW="${BASH_REMATCH[1]}"
|
||||
PREFIX="${BASH_REMATCH[2]}"
|
||||
BASE_3="${LAN_GW%.*}"
|
||||
LAN_CIDR="${BASE_3}.0/${PREFIX}"
|
||||
DHCP_RANGE_START="${BASE_3}.100"
|
||||
DHCP_RANGE_END="${BASE_3}.200"
|
||||
pct exec "$CTID" -- bash -c "mkdir -p /opt/cm4-provisioning && echo 'LAN_GW=$LAN_GW' > /opt/cm4-provisioning/lan-subnet.conf && echo 'LAN_CIDR=$LAN_CIDR' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_START=$DHCP_RANGE_START' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_END=$DHCP_RANGE_END' >> /opt/cm4-provisioning/lan-subnet.conf"
|
||||
log "LXC: wrote /opt/cm4-provisioning/lan-subnet.conf (LAN_GW=$LAN_GW, LAN_CIDR=$LAN_CIDR, DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END})"
|
||||
else
|
||||
log "Warning: DEPLOY_LXC_LAN_SUBNET=$LAN_SUBNET_FOR_CONF not in form A.B.C.D/PREFIX; skipping lan-subnet.conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- LXC: flash scripts (for reference; actual flash runs on host) ---
|
||||
log "LXC: installing flash scripts ..."
|
||||
pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning
|
||||
|
||||
130
emmc-provisioning/scripts/edit-cloudinit-on-image.sh
Executable file
130
emmc-provisioning/scripts/edit-cloudinit-on-image.sh
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mount a Raspberry Pi OS .img or .img.xz, edit cloud-init NoCloud files on the boot
|
||||
# partition, then unmount and (if .img.xz) recompress. Requires sudo for loop/mount.
|
||||
#
|
||||
# Usage:
|
||||
# ./edit-cloudinit-on-image.sh <path-to-image.img.xz>
|
||||
# ./edit-cloudinit-on-image.sh <path-to-image.img>
|
||||
#
|
||||
# Options:
|
||||
# --no-recompress If image was .img.xz, leave decompressed .img and do not overwrite
|
||||
# --replace-with-repo Copy user-data, meta-data, network-config from repo before editing
|
||||
#
|
||||
# Example:
|
||||
# ./edit-cloudinit-on-image.sh /path/to/gnss-bootstrap-20260223-215010.img.xz
|
||||
#
|
||||
# Backup the image before running if you want to keep the original.
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CLOUDINIT_SRC="$REPO_ROOT/emmc-provisioning/cloud-init"
|
||||
NO_RECOMPRESS=""
|
||||
REPLACE_WITH_REPO=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-recompress) NO_RECOMPRESS=1; shift ;;
|
||||
--replace-with-repo) REPLACE_WITH_REPO=1; shift ;;
|
||||
-*) echo "Unknown option: $1"; exit 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
IMAGE_IN="$1"
|
||||
[[ -n "$IMAGE_IN" && -f "$IMAGE_IN" ]] || {
|
||||
echo "Usage: $0 [--no-recompress] [--replace-with-repo] <path-to-image.img.xz|.img>"
|
||||
echo "Example: $0 gnss-bootstrap-20260223-215010.img.xz"
|
||||
exit 1
|
||||
}
|
||||
|
||||
IMAGE_IN="$(realpath "$IMAGE_IN")"
|
||||
WORK_DIR=""
|
||||
IMG_FILE=""
|
||||
ORIGINAL_XZ=""
|
||||
cleanup() {
|
||||
if [[ -n "$MNT" && -d "$MNT" ]]; then
|
||||
sudo umount "$MNT" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$LOOP" && -b "$LOOP" ]]; then
|
||||
sudo losetup -d "$LOOP" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
|
||||
if [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then
|
||||
echo "Work dir left at: $WORK_DIR"
|
||||
else
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "$IMAGE_IN" == *.img.xz ]]; then
|
||||
ORIGINAL_XZ="$IMAGE_IN"
|
||||
echo "Decompressing $(basename "$ORIGINAL_XZ") (this may take a minute and needs ~3GB free)…"
|
||||
WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX)
|
||||
IMG_FILE="$WORK_DIR/image.img"
|
||||
xz -T 0 -d -k -f -c "$ORIGINAL_XZ" > "$IMG_FILE"
|
||||
else
|
||||
WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX)
|
||||
IMG_FILE="$IMAGE_IN"
|
||||
fi
|
||||
|
||||
echo "Attaching image and mounting boot partition…"
|
||||
LOOP=$(sudo losetup -f --show -P "$IMG_FILE")
|
||||
# Force kernel to scan partition table so /dev/loopNp1, p2 etc. appear
|
||||
sudo partprobe "$LOOP" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
boot_part="${LOOP}p1"
|
||||
[[ -b "$boot_part" ]] || boot_part="${LOOP}p2"
|
||||
[[ -b "$boot_part" ]] || {
|
||||
echo "Boot partition not found on image. Partitions on image:"
|
||||
ls -la "${LOOP}"p* 2>/dev/null || true
|
||||
sudo fdisk -l "$IMG_FILE" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
MNT="$WORK_DIR/mnt"
|
||||
mkdir -p "$MNT"
|
||||
sudo mount "$boot_part" "$MNT"
|
||||
|
||||
if [[ -n "$REPLACE_WITH_REPO" && -d "$CLOUDINIT_SRC" ]]; then
|
||||
echo "Copying cloud-init files from repo into boot partition…"
|
||||
for f in user-data meta-data network-config; do
|
||||
if [[ -f "$CLOUDINIT_SRC/$f" ]]; then
|
||||
sudo cp "$CLOUDINIT_SRC/$f" "$MNT/$f"
|
||||
elif [[ -f "$CLOUDINIT_SRC/$f.bootstrap" ]]; then
|
||||
sudo cp "$CLOUDINIT_SRC/$f.bootstrap" "$MNT/$f"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Boot partition is mounted at: $MNT"
|
||||
echo "Cloud-init files to edit:"
|
||||
echo " $MNT/user-data"
|
||||
echo " $MNT/meta-data"
|
||||
echo " $MNT/network-config"
|
||||
echo ""
|
||||
EDITOR="${EDITOR:-nano}"
|
||||
echo "Opening editor ($EDITOR). Save and exit when done."
|
||||
read -r -p "Press Enter to open $EDITOR on these files…"
|
||||
sudo "$EDITOR" "$MNT/user-data" "$MNT/meta-data" "$MNT/network-config"
|
||||
|
||||
echo "Unmounting…"
|
||||
sudo umount "$MNT"
|
||||
MNT=""
|
||||
sudo losetup -d "$LOOP"
|
||||
LOOP=""
|
||||
|
||||
if [[ -n "$ORIGINAL_XZ" && -z "$NO_RECOMPRESS" ]]; then
|
||||
echo "Recompressing to $(basename "$ORIGINAL_XZ")…"
|
||||
xz -T 0 -z -f -k "$IMG_FILE"
|
||||
mv -f "${IMG_FILE}.xz" "$ORIGINAL_XZ"
|
||||
echo "Done. Updated: $ORIGINAL_XZ"
|
||||
rm -rf "$WORK_DIR"
|
||||
WORK_DIR=""
|
||||
elif [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then
|
||||
echo "Left decompressed image at: $IMG_FILE"
|
||||
echo "Recompress manually: xz -z -k \"$IMG_FILE\""
|
||||
fi
|
||||
@@ -25,7 +25,17 @@ if [[ -n "$TARGET" ]]; then
|
||||
fi
|
||||
|
||||
# --- Running inside the LXC from here ---
|
||||
echo "Configuring network boot (DHCP + TFTP on eth1, NAT via eth0) ..."
|
||||
# LAN subnet: use /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh when DEPLOY_LXC_LAN_SUBNET is set)
|
||||
LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf"
|
||||
if [[ -f "$LAN_CONF" ]]; then
|
||||
source "$LAN_CONF"
|
||||
else
|
||||
LAN_GW="10.20.50.1"
|
||||
LAN_CIDR="10.20.50.0/24"
|
||||
DHCP_RANGE_START="10.20.50.100"
|
||||
DHCP_RANGE_END="10.20.50.200"
|
||||
fi
|
||||
echo "Configuring network boot (DHCP + TFTP on eth1, NAT via eth0) — LAN $LAN_CIDR (gateway $LAN_GW), DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END} ..."
|
||||
|
||||
# 1) Install dnsmasq
|
||||
if ! command -v dnsmasq >/dev/null 2>&1; then
|
||||
@@ -34,12 +44,12 @@ fi
|
||||
|
||||
# 2) dnsmasq config for eth1 only (DHCP + TFTP); PXE options in network-boot-pxe.conf (toggle with toggle-network-boot-dhcp.sh)
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
cat > /etc/dnsmasq.d/network-boot.conf << 'DNSMASQ'
|
||||
cat > /etc/dnsmasq.d/network-boot.conf << DNSMASQ
|
||||
# DHCP on eth1 only (provisioning LAN)
|
||||
# TFTP and PXE options are in network-boot-pxe.conf, controlled by toggle-network-boot-dhcp.sh
|
||||
interface=eth1
|
||||
bind-interfaces
|
||||
dhcp-range=10.20.50.100,10.20.50.200,12h
|
||||
dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},12h
|
||||
log-dhcp
|
||||
log-queries
|
||||
port=0
|
||||
@@ -89,14 +99,14 @@ fi
|
||||
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-cm4-network-boot.conf
|
||||
sysctl -p /etc/sysctl.d/99-cm4-network-boot.conf 2>/dev/null || sysctl -w net.ipv4.ip_forward=1
|
||||
|
||||
# 5) NAT: 10.20.50.0/24 -> eth0 (masquerade)
|
||||
# 5) NAT: LAN subnet -> eth0 (masquerade)
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
mkdir -p /etc/nftables.d
|
||||
cat > /etc/nftables.d/nat-lan.conf << 'NFT'
|
||||
cat > /etc/nftables.d/nat-lan.conf << NFT
|
||||
table ip nat {
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
ip saddr 10.20.50.0/24 oifname "eth0" masquerade
|
||||
ip saddr ${LAN_CIDR} oifname "eth0" masquerade
|
||||
}
|
||||
}
|
||||
NFT
|
||||
@@ -110,8 +120,8 @@ NFT
|
||||
echo "NAT rule added (nftables) and saved to /etc/nftables.d/nat-lan.conf"
|
||||
else
|
||||
# Fallback iptables
|
||||
iptables -t nat -C POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE
|
||||
iptables -t nat -C POSTROUTING -s "${LAN_CIDR}" -o eth0 -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -s "${LAN_CIDR}" -o eth0 -j MASQUERADE
|
||||
echo "NAT rule added (iptables)."
|
||||
fi
|
||||
|
||||
@@ -120,6 +130,6 @@ systemctl enable dnsmasq
|
||||
systemctl restart dnsmasq
|
||||
|
||||
Reference in New Issue
Block a user