Add DHCP leases management to dashboard and UI
Implement a new API endpoint to retrieve current DHCP leases from dnsmasq, enhancing the dashboard's functionality for monitoring network devices. Update the home.html template to display DHCP lease information in a structured table format, including IP, MAC, hostname, and expiry details. Introduce buttons for enabling and disabling DHCP network boot, improving user interaction. Enhance JavaScript to fetch and display lease data dynamically, ensuring users have real-time visibility of network activity.
This commit is contained in:
@@ -42,6 +42,7 @@ CLOUDINIT_TEMPLATES_FILE = Path(os.environ.get("CM4_CLOUDINIT_TEMPLATES_FILE", s
|
|||||||
PORTAL_DESCRIPTIONS_FILE = Path(os.environ.get("CM4_PORTAL_DESCRIPTIONS_FILE", str(BASE_DIR / "portal_descriptions.json")))
|
PORTAL_DESCRIPTIONS_FILE = Path(os.environ.get("CM4_PORTAL_DESCRIPTIONS_FILE", str(BASE_DIR / "portal_descriptions.json")))
|
||||||
DB_PATH = Path(os.environ.get("CM4_DASHBOARD_DB", str(BASE_DIR / "dashboard.db")))
|
DB_PATH = Path(os.environ.get("CM4_DASHBOARD_DB", str(BASE_DIR / "dashboard.db")))
|
||||||
TOGGLE_NETWORK_BOOT_SCRIPT = os.environ.get("CM4_TOGGLE_NETWORK_BOOT_SCRIPT", "/opt/cm4-provisioning/toggle-network-boot-dhcp.sh")
|
TOGGLE_NETWORK_BOOT_SCRIPT = os.environ.get("CM4_TOGGLE_NETWORK_BOOT_SCRIPT", "/opt/cm4-provisioning/toggle-network-boot-dhcp.sh")
|
||||||
|
DHCP_LEASES_FILE = os.environ.get("CM4_DHCP_LEASES_FILE", "/var/lib/misc/dnsmasq.leases")
|
||||||
|
|
||||||
|
|
||||||
# --- Database (admin users + activity logs) ---
|
# --- Database (admin users + activity logs) ---
|
||||||
@@ -593,13 +594,46 @@ def api_dhcp_network_boot_post():
|
|||||||
@app.route("/api/action-done", methods=["POST"])
|
@app.route("/api/action-done", methods=["POST"])
|
||||||
def api_action_done():
|
def api_action_done():
|
||||||
"""Called by a device when deploy or backup has completed. Disables DHCP network-boot so the device boots from eMMC next time."""
|
"""Called by a device when deploy or backup has completed. Disables DHCP network-boot so the device boots from eMMC next time."""
|
||||||
mac = (request.args.get("mac") or request.get_json(silent=True) or {}).get("mac", "")
|
mac = request.args.get("mac") or ((request.get_json(silent=True) or {}).get("mac") or "")
|
||||||
ok, _ = _dhcp_network_boot_run("disable")
|
ok, _ = _dhcp_network_boot_run("disable")
|
||||||
if not ok:
|
if not ok:
|
||||||
return jsonify({"ok": False, "error": "Could not disable DHCP network boot"}), 500
|
return jsonify({"ok": False, "error": "Could not disable DHCP network boot"}), 500
|
||||||
return jsonify({"ok": True, "message": "Network boot disabled; device will boot from eMMC on next boot"})
|
return jsonify({"ok": True, "message": "Network boot disabled; device will boot from eMMC on next boot"})
|
||||||
|
|
||||||
|
|
||||||
|
def _read_dhcp_leases():
|
||||||
|
"""Read dnsmasq lease file. Returns (leases_list, error_string). leases_list items: {expiry, mac, ip, hostname}."""
|
||||||
|
if not DHCP_LEASES_FILE or not os.path.isfile(DHCP_LEASES_FILE):
|
||||||
|
return [], None
|
||||||
|
try:
|
||||||
|
leases = []
|
||||||
|
with open(DHCP_LEASES_FILE, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
leases.append({
|
||||||
|
"expiry": int(parts[0]) if parts[0].isdigit() else 0,
|
||||||
|
"mac": parts[1],
|
||||||
|
"ip": parts[2],
|
||||||
|
"hostname": parts[3] if len(parts) > 3 else "",
|
||||||
|
})
|
||||||
|
return leases, None
|
||||||
|
except (OSError, PermissionError) as e:
|
||||||
|
return [], str(e)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/dhcp-leases")
|
||||||
|
def api_dhcp_leases():
|
||||||
|
"""Return current DHCP leases from dnsmasq (when dashboard runs on LXC with dnsmasq)."""
|
||||||
|
leases, err = _read_dhcp_leases()
|
||||||
|
if err:
|
||||||
|
return jsonify({"leases": [], "error": err})
|
||||||
|
return jsonify({"leases": leases, "error": None})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/device-action-poll")
|
@app.route("/api/device-action-poll")
|
||||||
def api_device_action_poll():
|
def api_device_action_poll():
|
||||||
"""Network device polls this to get its assigned action (deploy/backup) and URL."""
|
"""Network device polls this to get its assigned action (deploy/backup) and URL."""
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
.steps-list .num { flex-shrink: 0; width: 1.2rem; height: 1.2rem; background: var(--bg-secondary); color: var(--text-dim); border-radius: 50%; font-size: 0.7rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
|
.steps-list .num { flex-shrink: 0; width: 1.2rem; height: 1.2rem; background: var(--bg-secondary); color: var(--text-dim); border-radius: 50%; font-size: 0.7rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
.steps-list strong { color: var(--text); }
|
.steps-list strong { color: var(--text); }
|
||||||
.help-sub { font-weight: 600; color: var(--text); margin: 0.5rem 0 0.2rem 0; font-size: 0.85rem; }
|
.help-sub { font-weight: 600; color: var(--text); margin: 0.5rem 0 0.2rem 0; font-size: 0.85rem; }
|
||||||
|
.leases-table { width: 100%; font-size: 0.8rem; border-collapse: collapse; }
|
||||||
|
.leases-table th, .leases-table td { padding: 0.35rem 0.5rem; text-align: left; border-bottom: 1px solid var(--border); }
|
||||||
|
.leases-table th { color: var(--text-dim); font-weight: 600; }
|
||||||
|
.leases-table .mono { font-family: 'JetBrains Mono', monospace; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -104,7 +108,7 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-title">Capture or deploy</h2>
|
<h2 class="card-title">Capture or deploy</h2>
|
||||||
<p id="dhcpNetbootWrap" class="status-row" style="margin-bottom: 0.5rem; font-size: 0.85rem;"><span class="text-dim">Network boot (DHCP):</span> <span id="dhcpNetbootState">—</span> <button type="button" id="dhcpNetbootDisableBtn" class="btn btn-outline btn-sm" style="display:none;">Disable network boot</button></p>
|
<p id="dhcpNetbootWrap" class="status-row" style="margin-bottom: 0.5rem; font-size: 0.85rem;"><span class="text-dim">Network boot (DHCP):</span> <span id="dhcpNetbootState">—</span> <button type="button" id="dhcpNetbootDisableBtn" class="btn btn-outline btn-sm" style="display:none;">Disable network boot</button> <button type="button" id="dhcpNetbootEnableBtn" class="btn btn-outline btn-sm" style="display:none;">Enable network boot</button></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>
|
<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 or register over network.</p>
|
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode or register over network.</p>
|
||||||
@@ -123,6 +127,16 @@
|
|||||||
<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;">
|
||||||
|
<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>
|
||||||
|
<div id="leasesContent"><table class="leases-table"><thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Expires</th></tr></thead><tbody id="leasesBody"></tbody></table></div>
|
||||||
|
<p id="leasesError" style="display:none; font-size: 0.8rem; color: var(--text-muted);"></p>
|
||||||
|
<p id="leasesEmpty" style="display:none; font-size: 0.85rem; color: var(--text-muted);">No leases file or no active leases.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details class="card" style="padding: 0; margin-top: 1rem;">
|
<details class="card" style="padding: 0; margin-top: 1rem;">
|
||||||
<summary>How to connect</summary>
|
<summary>How to connect</summary>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
@@ -154,6 +168,7 @@
|
|||||||
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'; }
|
||||||
var clearWrap = document.getElementById('statusClearWrap');
|
var clearWrap = document.getElementById('statusClearWrap');
|
||||||
if (clearWrap) clearWrap.style.display = (phase === 'error' ? 'block' : 'none');
|
if (clearWrap) clearWrap.style.display = (phase === 'error' ? 'block' : 'none');
|
||||||
|
if (phase === 'done') scheduleDoneClear();
|
||||||
const progress = data.progress;
|
const progress = data.progress;
|
||||||
const inProgress = ['rpiboot', 'flashing', 'backup'].includes(phase);
|
const inProgress = ['rpiboot', 'flashing', 'backup'].includes(phase);
|
||||||
const wrap = document.getElementById('progressWrap');
|
const wrap = document.getElementById('progressWrap');
|
||||||
@@ -225,12 +240,35 @@
|
|||||||
function fetchDhcpNetboot() {
|
function fetchDhcpNetboot() {
|
||||||
fetch('/api/dhcp-network-boot').then(function(r){ return r.json(); }).then(function(d){
|
fetch('/api/dhcp-network-boot').then(function(r){ return r.json(); }).then(function(d){
|
||||||
const stateEl = document.getElementById('dhcpNetbootState');
|
const stateEl = document.getElementById('dhcpNetbootState');
|
||||||
const btn = document.getElementById('dhcpNetbootDisableBtn');
|
const disableBtn = document.getElementById('dhcpNetbootDisableBtn');
|
||||||
if (d.error) { stateEl.textContent = '—'; if (btn) btn.style.display = 'none'; return; }
|
const enableBtn = document.getElementById('dhcpNetbootEnableBtn');
|
||||||
|
if (d.error) { stateEl.textContent = '—'; if (disableBtn) disableBtn.style.display = 'none'; if (enableBtn) enableBtn.style.display = 'none'; return; }
|
||||||
stateEl.textContent = d.enabled ? 'on' : 'off';
|
stateEl.textContent = d.enabled ? 'on' : 'off';
|
||||||
if (btn) btn.style.display = d.enabled ? 'inline-block' : 'none';
|
if (disableBtn) disableBtn.style.display = d.enabled ? 'inline-block' : 'none';
|
||||||
|
if (enableBtn) enableBtn.style.display = d.enabled ? 'none' : 'inline-block';
|
||||||
}).catch(function(){ document.getElementById('dhcpNetbootState').textContent = '—'; });
|
}).catch(function(){ document.getElementById('dhcpNetbootState').textContent = '—'; });
|
||||||
}
|
}
|
||||||
|
var doneClearTimer = null;
|
||||||
|
function scheduleDoneClear() {
|
||||||
|
if (doneClearTimer) return;
|
||||||
|
doneClearTimer = setTimeout(function(){ doneClearTimer = null; fetch('/api/status-clear', { method: 'POST' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchStatus(); }); }, 60000);
|
||||||
|
}
|
||||||
|
function fetchDhcpLeases() {
|
||||||
|
fetch('/api/dhcp-leases').then(function(r){ return r.json(); }).then(function(d){
|
||||||
|
const tbody = document.getElementById('leasesBody');
|
||||||
|
const errEl = document.getElementById('leasesError');
|
||||||
|
const emptyEl = document.getElementById('leasesEmpty');
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
emptyEl.style.display = 'none';
|
||||||
|
if (d.error) { errEl.textContent = d.error; errEl.style.display = 'block'; tbody.innerHTML = ''; return; }
|
||||||
|
var rows = (d.leases || []).map(function(l){
|
||||||
|
var expStr = l.expiry ? (new Date(l.expiry*1000).toLocaleString()) : '—';
|
||||||
|
return '<tr><td class="mono">' + escapeHtml(l.ip) + '</td><td class="mono">' + escapeHtml(l.mac) + '</td><td>' + escapeHtml(l.hostname || '') + '</td><td>' + expStr + '</td></tr>';
|
||||||
|
}).join('');
|
||||||
|
tbody.innerHTML = rows || '';
|
||||||
|
if (!rows) emptyEl.style.display = 'block';
|
||||||
|
}).catch(function(){ document.getElementById('leasesError').textContent = 'Could not load leases.'; document.getElementById('leasesError').style.display = 'block'; });
|
||||||
|
}
|
||||||
document.getElementById('statusClearBtn').addEventListener('click', function(){ fetch('/api/status-clear', { method: 'POST' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchStatus(); }); });
|
document.getElementById('statusClearBtn').addEventListener('click', function(){ fetch('/api/status-clear', { method: 'POST' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchStatus(); }); });
|
||||||
document.getElementById('dhcpNetbootDisableBtn').addEventListener('click', function(){
|
document.getElementById('dhcpNetbootDisableBtn').addEventListener('click', function(){
|
||||||
fetch('/api/dhcp-network-boot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: false }) })
|
fetch('/api/dhcp-network-boot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: false }) })
|
||||||
@@ -238,12 +276,19 @@
|
|||||||
.then(function(d){ if(d.ok) fetchDhcpNetboot(); else alert(d.error || 'Failed'); })
|
.then(function(d){ if(d.ok) fetchDhcpNetboot(); else alert(d.error || 'Failed'); })
|
||||||
.catch(function(){ alert('Request failed'); });
|
.catch(function(){ alert('Request failed'); });
|
||||||
});
|
});
|
||||||
fetchStatus(); fetchLog(); fetchPending(); fetchGolden(); fetchDhcpNetboot();
|
document.getElementById('dhcpNetbootEnableBtn').addEventListener('click', function(){
|
||||||
|
fetch('/api/dhcp-network-boot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true }) })
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(d){ if(d.ok) fetchDhcpNetboot(); else alert(d.error || 'Failed'); })
|
||||||
|
.catch(function(){ alert('Request failed'); });
|
||||||
|
});
|
||||||
|
fetchStatus(); fetchLog(); fetchPending(); fetchGolden(); fetchDhcpNetboot(); fetchDhcpLeases();
|
||||||
setInterval(fetchStatus, 2000);
|
setInterval(fetchStatus, 2000);
|
||||||
setInterval(fetchLog, 4000);
|
setInterval(fetchLog, 4000);
|
||||||
setInterval(fetchPending, 2000);
|
setInterval(fetchPending, 2000);
|
||||||
setInterval(fetchGolden, 10000);
|
setInterval(fetchGolden, 10000);
|
||||||
setInterval(fetchDhcpNetboot, 10000);
|
setInterval(fetchDhcpNetboot, 10000);
|
||||||
|
setInterval(fetchDhcpLeases, 10000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ To refresh or populate TFTP without re-running the full setup:
|
|||||||
|
|
||||||
The TFTP root contains e.g. `start4cd.elf`, `fixup4cd.dat`, `config.txt`, `cmdline.txt`, `kernel8.img`, and other boot files. For a custom kernel or initramfs (e.g. for provisioning), add or replace files in `/srv/tftpboot` and adjust `config.txt` / `cmdline.txt` as needed.
|
The TFTP root contains e.g. `start4cd.elf`, `fixup4cd.dat`, `config.txt`, `cmdline.txt`, `kernel8.img`, and other boot files. For a custom kernel or initramfs (e.g. for provisioning), add or replace files in `/srv/tftpboot` and adjust `config.txt` / `cmdline.txt` as needed.
|
||||||
|
|
||||||
|
## DHCP leases
|
||||||
|
|
||||||
|
On the LXC, dnsmasq stores DHCP leases in **`/var/lib/misc/dnsmasq.leases`** (Debian/Ubuntu default). To see which devices got an IP on the provisioning LAN:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the LXC (or via SSH)
|
||||||
|
cat /var/lib/misc/dnsmasq.leases
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line is: *expiry_epoch MAC IP hostname client_id*. Example: `1734567890 aa:bb:cc:dd:ee:ff 10.20.50.101 reterminal 01:aa:bb:cc:dd:ee:ff`
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
| Component | Where | Purpose |
|
| Component | Where | Purpose |
|
||||||
|
|||||||
@@ -205,6 +205,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" ) &
|
||||||
else
|
else
|
||||||
write_status "error" "Backup failed" "null" "dd failed"
|
write_status "error" "Backup failed" "null" "dd failed"
|
||||||
fi
|
fi
|
||||||
@@ -219,6 +220,8 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
|||||||
if dd if="$GOLDEN_IMAGE" of="$target_dev" bs=4M status=progress conv=fsync; then
|
if dd if="$GOLDEN_IMAGE" of="$target_dev" bs=4M status=progress conv=fsync; then
|
||||||
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)
|
||||||
|
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||||
else
|
else
|
||||||
write_status "error" "Flash failed" "null" "dd failed"
|
write_status "error" "Flash failed" "null" "dd failed"
|
||||||
fi
|
fi
|
||||||
|
|||||||
40
emmc-provisioning/scripts/check-network-boot-priority.sh
Normal file
40
emmc-provisioning/scripts/check-network-boot-priority.sh
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Check if network boot is set as first priority on a Pi 4 / CM4 (reTerminal).
|
||||||
|
# Run on the device: ./check-network-boot-priority.sh
|
||||||
|
# Or from your machine: ssh pi@<device-ip> 'bash -s' < scripts/check-network-boot-priority.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
# BOOT_ORDER: 0x2 = network, 0x1 = SD/eMMC. 0x21 = network first, then local storage.
|
||||||
|
WANT_BOOT_ORDER="0x21"
|
||||||
|
|
||||||
|
get_config() {
|
||||||
|
if command -v vcgencmd >/dev/null 2>&1; then
|
||||||
|
vcgencmd bootloader_config 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if command -v rpi-eeprom-update >/dev/null 2>&1 && command -v rpi-eeprom-config >/dev/null 2>&1; then
|
||||||
|
PEE="$(rpi-eeprom-update -l 2>/dev/null)"
|
||||||
|
if [[ -n "$PEE" && -f "$PEE" ]]; then
|
||||||
|
rpi-eeprom-config "$PEE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOTCONF="$(get_config)"
|
||||||
|
BOOT_ORDER="$(echo "$BOOTCONF" | grep -oE 'BOOT_ORDER=[0-9A-Fa-fx]+' | head -1 | cut -d= -f2)"
|
||||||
|
|
||||||
|
if [[ -z "$BOOT_ORDER" ]]; then
|
||||||
|
echo "Could not read EEPROM config (vcgencmd bootloader_config or rpi-eeprom-config)."
|
||||||
|
echo "Is this a Raspberry Pi 4 or CM4 with rpi-eeprom / vcgencmd?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "BOOT_ORDER=$BOOT_ORDER (current)"
|
||||||
|
echo "Expected for network first: $WANT_BOOT_ORDER (0x2=network, 0x1=SD/eMMC; 0x21 = network then local)"
|
||||||
|
|
||||||
|
if [[ "$(echo "$BOOT_ORDER" | tr '[:upper:]' '[:lower:]')" == "$(echo "$WANT_BOOT_ORDER" | tr '[:upper:]' '[:lower:]')" ]]; then
|
||||||
|
echo "Result: Network boot is set as first priority."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Result: Network boot is NOT first (current: $BOOT_ORDER). To set network first, set BOOT_ORDER=0x21 (e.g. via cloud-init first-boot or rpi-eeprom-config --edit)."
|
||||||
|
exit 2
|
||||||
@@ -12,6 +12,12 @@ if [[ -n "$TARGET" ]]; then
|
|||||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
echo "Syncing lxc config and script to $TARGET ..."
|
echo "Syncing lxc config and script to $TARGET ..."
|
||||||
rsync -a "$REPO_DIR/lxc/" "$TARGET:/tmp/cm4-network-boot-lxc/" --exclude='.git'
|
rsync -a "$REPO_DIR/lxc/" "$TARGET:/tmp/cm4-network-boot-lxc/" --exclude='.git'
|
||||||
|
if [[ -f "$REPO_DIR/network-boot-initramfs/initrd.img" ]]; then
|
||||||
|
echo "Copying initrd.img to $TARGET ..."
|
||||||
|
scp "$REPO_DIR/network-boot-initramfs/initrd.img" "$TARGET:/tmp/cm4-network-boot-lxc/initrd.img"
|
||||||
|
else
|
||||||
|
echo "Note: network-boot-initramfs/initrd.img not found (run build.sh first); skipping."
|
||||||
|
fi
|
||||||
scp "$SCRIPT_DIR/setup-network-boot-on-lxc.sh" "$TARGET:/tmp/cm4-network-boot-lxc/setup.sh"
|
scp "$SCRIPT_DIR/setup-network-boot-on-lxc.sh" "$TARGET:/tmp/cm4-network-boot-lxc/setup.sh"
|
||||||
ssh "$TARGET" "bash /tmp/cm4-network-boot-lxc/setup.sh"
|
ssh "$TARGET" "bash /tmp/cm4-network-boot-lxc/setup.sh"
|
||||||
echo "Done."
|
echo "Done."
|
||||||
@@ -68,6 +74,18 @@ else
|
|||||||
echo "TFTP root already has boot files (start4cd.elf present), skipping fetch."
|
echo "TFTP root already has boot files (start4cd.elf present), skipping fetch."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 3b) Copy provisioning initrd.img to TFTP root if provided
|
||||||
|
if [[ -f /tmp/cm4-network-boot-lxc/initrd.img ]]; then
|
||||||
|
cp /tmp/cm4-network-boot-lxc/initrd.img /srv/tftpboot/initrd.img
|
||||||
|
echo "Copied initrd.img to /srv/tftpboot"
|
||||||
|
if [[ -f /srv/tftpboot/config.txt ]] && ! grep -q 'initramfs initrd.img' /srv/tftpboot/config.txt 2>/dev/null; then
|
||||||
|
echo "" >> /srv/tftpboot/config.txt
|
||||||
|
echo "# Provisioning initramfs (network-boot-initramfs)" >> /srv/tftpboot/config.txt
|
||||||
|
echo "initramfs initrd.img followkernel" >> /srv/tftpboot/config.txt
|
||||||
|
echo "Added initramfs line to config.txt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 4) IP forwarding (LAN clients use WAN)
|
# 4) IP forwarding (LAN clients use WAN)
|
||||||
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
|
||||||
@@ -105,4 +123,4 @@ systemctl restart dnsmasq
|
|||||||
echo "Network boot setup done."
|
echo "Network boot setup done."
|
||||||
echo " - DHCP + TFTP on eth1 (10.20.50.1), range 10.20.50.100-200"
|
echo " - DHCP + TFTP on eth1 (10.20.50.1), range 10.20.50.100-200"
|
||||||
echo " - NAT: 10.20.50.0/24 -> eth0 (internet)"
|
echo " - NAT: 10.20.50.0/24 -> eth0 (internet)"
|
||||||
echo " - TFTP root: /srv/tftpboot (RPi boot files from GitHub)"
|
echo " - TFTP root: /srv/tftpboot (RPi boot files; initrd.img if provided)"
|
||||||
|
|||||||
Reference in New Issue
Block a user