diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index ea2439b..5173953 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -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"}) diff --git a/emmc-provisioning/dashboard/templates/home.html b/emmc-provisioning/dashboard/templates/home.html index db49b20..8f009b8 100644 --- a/emmc-provisioning/dashboard/templates/home.html +++ b/emmc-provisioning/dashboard/templates/home.html @@ -104,6 +104,7 @@
Network boot (DHCP): —
@@ -182,7 +183,7 @@ hasAny = true; const el = document.createElement('div'); el.className = 'device-item'; - el.innerHTML = '