Add DHCP network boot management to API and UI
Enhance the dashboard API with new endpoints for managing DHCP network boot options, allowing devices to enable or disable network boot via POST requests. Update the device action handling to include a 'reboot' action, specifically for network devices. Modify the home.html template to display the current state of network boot and provide a button for disabling it. Update provisioning scripts to disable network boot after deployment or backup completion, ensuring devices boot from eMMC on the next startup. Improve user feedback and error handling throughout the changes.
This commit is contained in:
@@ -10,6 +10,7 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from functools import wraps
|
||||
@@ -40,6 +41,7 @@ SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR
|
||||
CLOUDINIT_TEMPLATES_FILE = Path(os.environ.get("CM4_CLOUDINIT_TEMPLATES_FILE", str(BASE_DIR / "cloudinit_templates.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")))
|
||||
TOGGLE_NETWORK_BOOT_SCRIPT = os.environ.get("CM4_TOGGLE_NETWORK_BOOT_SCRIPT", "/opt/cm4-provisioning/toggle-network-boot-dhcp.sh")
|
||||
|
||||
|
||||
# --- Database (admin users + activity logs) ---
|
||||
@@ -489,8 +491,10 @@ def api_device_action():
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
source = (body.get("source") or "").strip().lower()
|
||||
action = (body.get("action") or "").strip().lower()
|
||||
if action not in ("backup", "deploy"):
|
||||
return jsonify({"ok": False, "error": "action must be 'backup' or 'deploy'"}), 400
|
||||
if action not in ("backup", "deploy", "reboot"):
|
||||
return jsonify({"ok": False, "error": "action must be 'backup', 'deploy', or 'reboot'"}), 400
|
||||
if action == "reboot" and source != "network":
|
||||
return jsonify({"ok": False, "error": "'reboot' is only for network devices"}), 400
|
||||
if source == "usb":
|
||||
try:
|
||||
os.makedirs(os.path.dirname(ACTION_REQUEST_FILE) or ".", exist_ok=True)
|
||||
@@ -543,6 +547,59 @@ def api_register_device():
|
||||
return jsonify({"ok": True, "message": "registered"})
|
||||
|
||||
|
||||
def _dhcp_network_boot_run(cmd):
|
||||
"""Run toggle script with enable|disable|status. Returns (ok, output_or_error)."""
|
||||
if not os.path.isfile(TOGGLE_NETWORK_BOOT_SCRIPT) or not os.access(TOGGLE_NETWORK_BOOT_SCRIPT, os.X_OK):
|
||||
return False, "Toggle script not installed"
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[TOGGLE_NETWORK_BOOT_SCRIPT, cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return False, (out.stderr or out.stdout or "script failed").strip()
|
||||
return True, (out.stdout or "").strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Timeout"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
@app.route("/api/dhcp-network-boot", methods=["GET"])
|
||||
def api_dhcp_network_boot_get():
|
||||
"""Return whether DHCP network-boot options (66/67) are enabled."""
|
||||
ok, out = _dhcp_network_boot_run("status")
|
||||
if not ok:
|
||||
return jsonify({"enabled": None, "error": out}), 200
|
||||
return jsonify({"enabled": out.strip().lower() == "enabled"})
|
||||
|
||||
|
||||
@app.route("/api/dhcp-network-boot", methods=["POST"])
|
||||
def api_dhcp_network_boot_post():
|
||||
"""Enable or disable DHCP network-boot options (DHCP server keeps running). Body: { \"enabled\": true|false }."""
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
enabled = body.get("enabled")
|
||||
if enabled is None:
|
||||
return jsonify({"ok": False, "error": "enabled required (true|false)"}), 400
|
||||
cmd = "enable" if enabled else "disable"
|
||||
ok, out = _dhcp_network_boot_run(cmd)
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": out}), 500
|
||||
return jsonify({"ok": True, "enabled": enabled})
|
||||
|
||||
|
||||
@app.route("/api/action-done", methods=["POST"])
|
||||
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."""
|
||||
mac = (request.args.get("mac") or request.get_json(silent=True) or {}).get("mac", "")
|
||||
ok, _ = _dhcp_network_boot_run("disable")
|
||||
if not ok:
|
||||
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"})
|
||||
|
||||
|
||||
@app.route("/api/device-action-poll")
|
||||
def api_device_action_poll():
|
||||
"""Network device polls this to get its assigned action (deploy/backup) and URL."""
|
||||
@@ -558,6 +615,8 @@ def api_device_action_poll():
|
||||
return jsonify({"action": "deploy", "url": f"{base}/api/golden-image"})
|
||||
if action == "backup":
|
||||
return jsonify({"action": "backup", "upload_url": f"{base}/api/backup-upload?mac={mac}"})
|
||||
if action == "reboot":
|
||||
return jsonify({"action": "reboot"})
|
||||
return jsonify({"action": "wait"})
|
||||
return jsonify({"action": "wait"})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user