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/
|
emmc-provisioning/
|
||||||
├── README.md ← You are here
|
├── README.md ← You are here
|
||||||
├── docs/ Documentation
|
├── docs/ Documentation
|
||||||
|
│ ├── DEPLOY-NEW-PROXMOX.md Step-by-step: deploy to a new Proxmox instance
|
||||||
│ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage
|
│ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage
|
||||||
│ ├── NETWORK-BOOT-LXC.md Network boot (PXE/dnsmasq) and LXC
|
│ ├── 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
|
│ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference
|
||||||
├── host/ Scripts that run on the provisioning host (Proxmox host)
|
├── host/ Scripts that run on the provisioning host (Proxmox host)
|
||||||
│ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd
|
│ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd
|
||||||
@@ -53,6 +54,7 @@ emmc-provisioning/
|
|||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. **Read** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for setup and usage.
|
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).
|
2. **Deploy to a new Proxmox:** Follow [docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md) for clear step-by-step instructions.
|
||||||
3. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide).
|
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. 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**.
|
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
|
#!/bin/bash
|
||||||
# Minimal bootstrap script for cloud-init first boot (test).
|
# Minimal bootstrap script for cloud-init first boot (test).
|
||||||
set -e
|
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
|
echo "[$(date -Iseconds)] test completed" | tee -a /var/log/cloud-init-bootstrap.log
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
package_update: true
|
package_update: true
|
||||||
package_upgrade: false
|
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:
|
packages:
|
||||||
- curl
|
- curl
|
||||||
|
|
||||||
@@ -19,7 +25,76 @@ write_files:
|
|||||||
PasswordAuthentication yes
|
PasswordAuthentication yes
|
||||||
PermitRootLogin no
|
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:
|
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 enable ssh
|
||||||
- systemctl start ssh
|
- systemctl start ssh
|
||||||
# Download and run bootstrap script (edit URL to match your file server)
|
# Download and run bootstrap script (edit URL to match your file server)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<p id="goldenInfo" class="mono" style="font-size:0.9rem;">Loading…</p>
|
||||||
</div>
|
</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 -->
|
<!-- Backups -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">
|
<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); });
|
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();
|
fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden();
|
||||||
setInterval(fetchLogs, 30000);
|
setInterval(fetchLogs, 30000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>CM4 eMMC Provisioning</h1>
|
<h1>GNSS Guard Provisioning</h1>
|
||||||
<a href="/admin">Admin</a>
|
<a href="/admin">Admin</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
<h2 class="card-title">Status</h2>
|
<h2 class="card-title">Status</h2>
|
||||||
<div id="status" class="status-row">
|
<div id="status" class="status-row">
|
||||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
<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>
|
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<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>
|
<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>
|
<div id="pendingDevices"></div>
|
||||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p>
|
<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>
|
<p id="goldenInfo" class="golden-info">Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="card" style="padding: 0;">
|
<details class="card" style="padding: 0;" open>
|
||||||
<summary>Recent log</summary>
|
<summary>Recent log</summary>
|
||||||
<div class="inner"><pre id="log" class="log-pre"></pre></div>
|
<div class="inner"><pre id="log" class="log-pre"></pre></div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="card" style="padding: 0; margin-top: 1rem;">
|
<details class="card" style="padding: 0; margin-top: 1rem;" open>
|
||||||
<summary>DHCP leases</summary>
|
<summary>DHCP leases</summary>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p>
|
<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">
|
<div class="inner">
|
||||||
<p class="help-sub">USB boot</p>
|
<p class="help-sub">USB boot</p>
|
||||||
<ol class="steps-list">
|
<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">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>
|
<li><span class="num">3</span> Remove jumper and power cycle when done.</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
const phase = data.phase || 'idle';
|
const phase = data.phase || 'idle';
|
||||||
document.getElementById('statusPill').className = 'status-pill ' + phase;
|
document.getElementById('statusPill').className = 'status-pill ' + phase;
|
||||||
document.getElementById('statusPill').textContent = phaseLabels[phase] || 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');
|
const err = document.getElementById('statusErr');
|
||||||
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
|
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
|
||||||
if (phase === 'done') scheduleDoneClear();
|
if (phase === 'done') scheduleDoneClear();
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
shrinkWrap.style.display = 'block';
|
shrinkWrap.style.display = 'block';
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'device-item';
|
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);
|
container.appendChild(el);
|
||||||
} else shrinkWrap.style.display = 'none';
|
} else shrinkWrap.style.display = 'none';
|
||||||
noPending.style.display = hasAny ? 'none' : 'block';
|
noPending.style.display = hasAny ? 'none' : 'block';
|
||||||
@@ -212,10 +212,6 @@
|
|||||||
btn.onclick = function() {
|
btn.onclick = function() {
|
||||||
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
|
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
|
||||||
if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
|
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;
|
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) })
|
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||||
.then(function(r) { return r.json(); })
|
.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(){});
|
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 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() {
|
function fetchGolden() {
|
||||||
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
|
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
|
||||||
const el = document.getElementById('goldenInfo');
|
const el = document.getElementById('goldenInfo');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>CM4 eMMC Provisioning</h1>
|
<h1>GNSS Guard Provisioning</h1>
|
||||||
<p>Deploy or backup reTerminal via USB boot mode</p>
|
<p>Deploy or backup device via USB boot mode</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 1. Current status -->
|
<!-- 1. Current status -->
|
||||||
@@ -368,12 +368,12 @@
|
|||||||
<h2 class="section-title">Current status</h2>
|
<h2 class="section-title">Current status</h2>
|
||||||
<div id="status" class="status-row">
|
<div id="status" class="status-row">
|
||||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
<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>
|
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="statusErr" class="status-err" style="display:none;"></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;">
|
<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>
|
<button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="statusMeta" class="status-meta" style="display:none;"></div>
|
<div id="statusMeta" class="status-meta" style="display:none;"></div>
|
||||||
@@ -384,8 +384,8 @@
|
|||||||
|
|
||||||
<!-- 2. Capture (Backup) or Deploy -->
|
<!-- 2. Capture (Backup) or Deploy -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Capture image or deploy</h2>
|
<h2 class="section-title">Deploy and Backup</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>
|
<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;">
|
<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>
|
<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>
|
</p>
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>— USB boot mode</span>
|
<span>— USB boot mode</span>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- 3. Saved backups -->
|
<!-- 3. Saved backups -->
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
<div class="inner">
|
<div class="inner">
|
||||||
<p class="help-sub">USB boot mode</p>
|
<p class="help-sub">USB boot mode</p>
|
||||||
<ol class="steps-list">
|
<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">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>
|
<li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -485,7 +485,7 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- 5. Recent log (collapsible) -->
|
<!-- 5. Recent log (collapsible) -->
|
||||||
<details class="section" style="padding:0;">
|
<details class="section" style="padding:0;" open>
|
||||||
<summary>Recent log</summary>
|
<summary>Recent log</summary>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<pre id="log" class="log-pre"></pre>
|
<pre id="log" class="log-pre"></pre>
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
const phase = data.phase || 'idle';
|
const phase = data.phase || 'idle';
|
||||||
statusPill.className = 'status-pill ' + phase;
|
statusPill.className = 'status-pill ' + phase;
|
||||||
statusPill.textContent = phaseLabels[phase] || 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) {
|
if (data.error) {
|
||||||
statusErr.textContent = data.error;
|
statusErr.textContent = data.error;
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
if (shrinkWrap) shrinkWrap.style.display = 'block';
|
if (shrinkWrap) shrinkWrap.style.display = 'block';
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'device-item';
|
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);
|
container.appendChild(el);
|
||||||
} else {
|
} else {
|
||||||
if (shrinkWrap) shrinkWrap.style.display = 'none';
|
if (shrinkWrap) shrinkWrap.style.display = 'none';
|
||||||
@@ -583,10 +583,6 @@
|
|||||||
const mac = btn.getAttribute('data-mac');
|
const mac = btn.getAttribute('data-mac');
|
||||||
const body = { source: source, action: action };
|
const body = { source: source, action: action };
|
||||||
if (mac) body.mac = mac;
|
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');
|
const shrinkCb = document.getElementById('shrinkAfterBackup');
|
||||||
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
|
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
|
||||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
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, []); });
|
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() {
|
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() {
|
function fetchBackups() {
|
||||||
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {
|
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h1>Admin login</h1>
|
<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 %}
|
{% if error %}<p class="err">{{ error }}</p>{% endif %}
|
||||||
<form method="post" action="{{ url_for('login') }}">
|
<form method="post" action="{{ url_for('login') }}">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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:
|
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)
|
## What to edit (typical)
|
||||||
|
|
||||||
- **meta-data**
|
- **meta-data**
|
||||||
|
|||||||
@@ -48,7 +48,13 @@ The script will:
|
|||||||
|
|
||||||
## Proxmox: adding eth1 to the LXC
|
## 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.:
|
1. On the **Proxmox host**, add a second network device to the container, e.g.:
|
||||||
```bash
|
```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.
|
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
|
## 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.
|
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.
|
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
|
## One-command deploy
|
||||||
|
|
||||||
From your repo, a single run deploys **all** host and LXC files (scripts, systemd units, udev, dashboard):
|
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>`).
|
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.
|
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
|
## 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. **"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`).
|
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
|
fi
|
||||||
log "Backup complete: $BACKUPS_DIR/$final_name"
|
log "Backup complete: $BACKUPS_DIR/$final_name"
|
||||||
write_status "done" "Backup complete: $final_name" "100"
|
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
|
else
|
||||||
write_status "error" "Backup failed" "null" "dd failed"
|
write_status "error" "Backup failed" "null" "dd failed"
|
||||||
fi
|
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."
|
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"
|
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)
|
# 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
|
else
|
||||||
write_status "error" "Flash failed" "null" "dd failed"
|
write_status "error" "Flash failed" "null" "dd failed"
|
||||||
fi
|
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."
|
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"
|
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"
|
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
|
else
|
||||||
write_status "error" "EEPROM update failed" "null" "Failed to copy pieeprom.upd/sig"
|
write_status "error" "EEPROM update failed" "null" "Failed to copy pieeprom.upd/sig"
|
||||||
fi
|
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).
|
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 |
|
| 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`. |
|
| **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** | nftables NAT so 10.20.50.0/24 uses eth0 for internet. Applied by the setup script to `/etc/nftables.d/nat-lan.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):
|
Setup is done by running (from your machine):
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# PXE/network-boot DHCP options (option 66 = next-server, 67 = boot file).
|
# 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.
|
# 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
|
# 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=66,10.20.50.1
|
||||||
dhcp-option=67,start4cd.elf
|
dhcp-option=67,start4cd.elf
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# dnsmasq: DHCP + TFTP on eth1 only (provisioning LAN).
|
# dnsmasq: DHCP + TFTP on eth1 only (provisioning LAN).
|
||||||
# Install to /etc/dnsmasq.d/network-boot.conf on the LXC.
|
# Install to /etc/dnsmasq.d/network-boot.conf on the LXC.
|
||||||
# Restrict to eth1 so we don't interfere with host/other DHCP.
|
# 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)
|
# Listen only on eth1 (provisioning LAN)
|
||||||
interface=eth1
|
interface=eth1
|
||||||
bind-interfaces
|
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
|
dhcp-range=10.20.50.100,10.20.50.200,12h
|
||||||
|
|
||||||
# TFTP for Raspberry Pi / CM4 network boot
|
# TFTP for Raspberry Pi / CM4 network boot
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# nftables: NAT for LAN (eth1) so clients use WAN (eth0) for internet.
|
# nftables: NAT for LAN (eth1) so clients use WAN (eth0) for internet.
|
||||||
# Load with: nft -f /etc/nftables.d/nat-lan.conf
|
# 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 {
|
table ip nat {
|
||||||
chain postrouting {
|
chain postrouting {
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
# When disabled, TFTP is stopped and no boot server is advertised; DHCP still runs.
|
# When disabled, TFTP is stopped and no boot server is advertised; DHCP still runs.
|
||||||
# Usage: toggle-network-boot-dhcp.sh enable | disable | status
|
# Usage: toggle-network-boot-dhcp.sh enable | disable | status
|
||||||
# Run as root. Install to /opt/cm4-provisioning/toggle-network-boot-dhcp.sh
|
# 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
|
set -e
|
||||||
PXE_CONF="/etc/dnsmasq.d/network-boot-pxe.conf"
|
PXE_CONF="/etc/dnsmasq.d/network-boot-pxe.conf"
|
||||||
MAIN_CONF="/etc/dnsmasq.d/network-boot.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)
|
# Remove enable-tftp / tftp-root from main config if present (legacy; these belong in PXE conf)
|
||||||
cleanup_main_conf() {
|
cleanup_main_conf() {
|
||||||
if [ -f "$MAIN_CONF" ] && grep -q 'enable-tftp\|tftp-root' "$MAIN_CONF" 2>/dev/null; then
|
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
|
case "${1:-}" in
|
||||||
enable)
|
enable)
|
||||||
cleanup_main_conf
|
cleanup_main_conf
|
||||||
cat > "$PXE_CONF" << 'EOF'
|
cat > "$PXE_CONF" << EOF
|
||||||
# PXE/network boot ENABLED - managed by toggle-network-boot-dhcp.sh
|
# PXE/network boot ENABLED - managed by toggle-network-boot-dhcp.sh
|
||||||
# TFTP server (only active when network boot is enabled)
|
# TFTP server (only active when network boot is enabled)
|
||||||
enable-tftp
|
enable-tftp
|
||||||
tftp-root=/srv/tftpboot
|
tftp-root=/srv/tftpboot
|
||||||
# BOOTP fields (siaddr = TFTP server, filename = boot file)
|
# 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 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
|
dhcp-option=67,start4cd.elf
|
||||||
EOF
|
EOF
|
||||||
systemctl restart dnsmasq 2>/dev/null || service dnsmasq restart 2>/dev/null || true
|
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)
|
disable)
|
||||||
cleanup_main_conf
|
cleanup_main_conf
|
||||||
|
|||||||
@@ -23,6 +23,12 @@
|
|||||||
# DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH
|
# 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_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_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.
|
# 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) ..."
|
log "[4/5] Running remote install (host + LXC) ..."
|
||||||
|
|
||||||
# Pass optional LXC SSH vars (base64) and selected storage
|
# 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:-}'" bash -s << 'REMOTE'
|
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
|
set -e
|
||||||
DEPLOY=/tmp/emmc-provisioning-deploy
|
DEPLOY=/tmp/emmc-provisioning-deploy
|
||||||
ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}"
|
ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}"
|
||||||
@@ -185,14 +191,24 @@ else
|
|||||||
fi
|
fi
|
||||||
[[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; }
|
[[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; }
|
||||||
TMPL_NAME=$(basename "$DEBIAN12_TMPL")
|
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=""
|
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"
|
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
|
fi
|
||||||
pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \
|
pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \
|
||||||
--hostname "$LXC_HOSTNAME" --memory 1024 --swap 0 --cores 1 \
|
--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
|
--unprivileged 0 --features nesting=1 -tag cm4-provisioning
|
||||||
mkdir -p /var/lib/cm4-provisioning
|
mkdir -p /var/lib/cm4-provisioning
|
||||||
pct set "$CTID" -mp0 /var/lib/cm4-provisioning,mp=/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 ..."
|
log "Starting LXC $CTID if stopped ..."
|
||||||
pct start "$CTID" 2>/dev/null || true
|
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) ---
|
# --- LXC: flash scripts (for reference; actual flash runs on host) ---
|
||||||
log "LXC: installing flash scripts ..."
|
log "LXC: installing flash scripts ..."
|
||||||
pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning
|
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
|
fi
|
||||||
|
|
||||||
# --- Running inside the LXC from here ---
|
# --- 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
|
# 1) Install dnsmasq
|
||||||
if ! command -v dnsmasq >/dev/null 2>&1; then
|
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)
|
# 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
|
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)
|
# DHCP on eth1 only (provisioning LAN)
|
||||||
# TFTP and PXE options are in network-boot-pxe.conf, controlled by toggle-network-boot-dhcp.sh
|
# TFTP and PXE options are in network-boot-pxe.conf, controlled by toggle-network-boot-dhcp.sh
|
||||||
interface=eth1
|
interface=eth1
|
||||||
bind-interfaces
|
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-dhcp
|
||||||
log-queries
|
log-queries
|
||||||
port=0
|
port=0
|
||||||
@@ -89,14 +99,14 @@ fi
|
|||||||
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-cm4-network-boot.conf
|
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
|
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
|
if command -v nft >/dev/null 2>&1; then
|
||||||
mkdir -p /etc/nftables.d
|
mkdir -p /etc/nftables.d
|
||||||
cat > /etc/nftables.d/nat-lan.conf << 'NFT'
|
cat > /etc/nftables.d/nat-lan.conf << NFT
|
||||||
table ip nat {
|
table ip nat {
|
||||||
chain postrouting {
|
chain postrouting {
|
||||||
type nat hook postrouting priority srcnat; policy accept;
|
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
|
NFT
|
||||||
@@ -110,8 +120,8 @@ NFT
|
|||||||
echo "NAT rule added (nftables) and saved to /etc/nftables.d/nat-lan.conf"
|
echo "NAT rule added (nftables) and saved to /etc/nftables.d/nat-lan.conf"
|
||||||
else
|
else
|
||||||
# Fallback iptables
|
# Fallback iptables
|
||||||
iptables -t nat -C POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
|
iptables -t nat -C POSTROUTING -s "${LAN_CIDR}" -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 -A POSTROUTING -s "${LAN_CIDR}" -o eth0 -j MASQUERADE
|
||||||
echo "NAT rule added (iptables)."
|
echo "NAT rule added (iptables)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -120,6 +130,6 @@ systemctl enable dnsmasq
|
|||||||
systemctl restart dnsmasq
|
systemctl restart dnsmasq
|
||||||
|
|||||||
Reference in New Issue
Block a user