Refactor first-boot process to introduce ordered execution and new one-shot scripts
Revise the first-boot script to implement a structured approach with 13 numbered steps, allowing for better control over the execution order. Introduce two new one-shot scripts: `01-set-rotation-once.sh` and `02-set-wallpaper-once.sh`, replacing the previous single script approach. Update documentation to reflect these changes, including the new configuration options for enabling/disabling steps and the revised file structure for one-shot scripts. Enhance the dashboard to display first-boot progress, improving user feedback during the initial setup.
This commit is contained in:
@@ -46,6 +46,7 @@ BUILD_STATUS_FILE = Path(os.environ.get("CM4_BUILD_STATUS_FILE", str(BASE_DIR /
|
||||
BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR / "build_cloudinit_request.json")))
|
||||
SHRINK_REQUEST_FILE = Path(os.environ.get("CM4_SHRINK_REQUEST_FILE", str(BASE_DIR / "shrink_request.json")))
|
||||
SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR / "shrink_status.json")))
|
||||
FIRST_BOOT_STATUS_FILE = Path(os.environ.get("CM4_FIRST_BOOT_STATUS_FILE", str(BASE_DIR / "first_boot_status.json")))
|
||||
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")))
|
||||
@@ -587,6 +588,59 @@ def api_status_clear():
|
||||
return jsonify({"ok": False, "error": "Could not write status"}), 500
|
||||
|
||||
|
||||
def _first_boot_status_read():
|
||||
"""Read first-boot status (device reports progress during first-boot.sh)."""
|
||||
try:
|
||||
with open(FIRST_BOOT_STATUS_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {"phase": "idle", "message": "", "step": "", "step_name": "", "hostname": "", "ip": "", "updated": None}
|
||||
|
||||
|
||||
def _first_boot_status_write(phase, message="", step="", step_name="", hostname="", ip="", error=None):
|
||||
"""Write first-boot status (called by POST from device)."""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(FIRST_BOOT_STATUS_FILE) or ".", exist_ok=True)
|
||||
data = {
|
||||
"phase": phase,
|
||||
"message": message,
|
||||
"step": step,
|
||||
"step_name": step_name,
|
||||
"hostname": hostname,
|
||||
"ip": ip or "",
|
||||
"updated": time.time(),
|
||||
}
|
||||
if error:
|
||||
data["error"] = error
|
||||
with open(FIRST_BOOT_STATUS_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
@app.route("/api/first-boot-status", methods=["GET"])
|
||||
def api_first_boot_status_get():
|
||||
"""Return current first-boot progress (for dashboard to poll)."""
|
||||
return jsonify(_first_boot_status_read())
|
||||
|
||||
|
||||
@app.route("/api/first-boot-status", methods=["POST"])
|
||||
def api_first_boot_status_post():
|
||||
"""Called by device during first-boot.sh to report progress. No auth (device on local net)."""
|
||||
body = request.get_json(silent=True) or {}
|
||||
phase = (body.get("phase") or "running").strip().lower()
|
||||
if phase not in ("started", "running", "done", "error"):
|
||||
phase = "running"
|
||||
message = (body.get("message") or "").strip()[:500]
|
||||
step = (body.get("step") or "").strip()[:10]
|
||||
step_name = (body.get("step_name") or "").strip()[:80]
|
||||
hostname = (body.get("hostname") or "").strip()[:64]
|
||||
ip = (body.get("ip") or "").strip()[:45]
|
||||
error = (body.get("error") or "").strip()[:500] if phase == "error" else None
|
||||
_first_boot_status_write(phase=phase, message=message, step=step, step_name=step_name, hostname=hostname, ip=ip, error=error)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/log")
|
||||
def api_log():
|
||||
return jsonify({"log": read_log_tail()})
|
||||
@@ -653,6 +707,8 @@ def api_device_action():
|
||||
pass # host may still have SHRINK_BACKUP=1
|
||||
with open(ACTION_REQUEST_FILE, "w") as f:
|
||||
f.write(action)
|
||||
if action == "deploy":
|
||||
_first_boot_status_write("idle", "", hostname="", ip="")
|
||||
return jsonify({"ok": True})
|
||||
except (PermissionError, OSError):
|
||||
return jsonify({"ok": False, "error": "Could not write action file"}), 500
|
||||
@@ -669,6 +725,7 @@ def api_device_action():
|
||||
ip = d.get("ip") or mac
|
||||
if action == "deploy":
|
||||
_write_status("flashing", f"Deploying to {ip} (network)...")
|
||||
_first_boot_status_write("idle", "", hostname="", ip="")
|
||||
elif action == "backup":
|
||||
_write_status("backup", f"Backing up {ip} (network)...")
|
||||
return jsonify({"ok": True})
|
||||
|
||||
Reference in New Issue
Block a user