#!/usr/bin/env python3 """ Cezen AI Suite — First Boot Web Setup Server Serves on port 80. Access from any browser on the same network. """ import os, json, subprocess, threading, time, socket, ipaddress from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import parse_qs, urlparse SETUP_DONE_FILE = "/opt/cezen/.setup-done" INSTALL_LOG = "/var/log/cezen-install.log" AIPACKAGE_DIR = "/opt/aipackage" install_proc = None install_status = {"running": False, "done": False, "error": None} # ─── Helpers ────────────────────────────────────────────── def get_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except: return "unknown" def get_interfaces(): try: out = subprocess.check_output(["ip", "-o", "link", "show"], text=True) ifaces = [] for line in out.splitlines(): name = line.split(": ")[1].split("@")[0] if name not in ("lo",) and not name.startswith(("docker","br-","veth","k3s")): ifaces.append(name) return ifaces except: return ["eth0"] def apply_static_ip(iface, ip, prefix, gateway, dns): config = f"""network: version: 2 ethernets: {iface}: dhcp4: false addresses: - {ip}/{prefix} routes: - to: default via: {gateway} nameservers: addresses: [{dns}] """ with open("/etc/netplan/99-cezen-static.yaml", "w") as f: f.write(config) subprocess.run(["netplan", "apply"], capture_output=True) time.sleep(3) def run_install(tier, skip_tools): global install_status install_status = {"running": True, "done": False, "error": None} try: # Write config so phase 2 (post-reboot) knows what to skip os.makedirs("/opt/cezen", exist_ok=True) skip_str = ",".join(skip_tools) if skip_tools else "" with open("/opt/cezen/install.conf", "w") as f: f.write(f"TIER={tier}\nSKIP_ROLES={skip_str}\n") # Mark setup done NOW so this web UI doesn't restart after the phase-1 reboot open(SETUP_DONE_FILE, "w").close() env = os.environ.copy() # Phase 1: installs NVIDIA drivers, registers cezen-phase2 systemd service, # then reboots. Phase 2 (full stack) runs automatically after reboot. cmd = ["bash", f"{AIPACKAGE_DIR}/install.sh", "--phase=1", f"--tier={tier}"] with open(INSTALL_LOG, "w") as log: proc = subprocess.Popen(cmd, stdout=log, stderr=log, env=env) proc.wait() # Reaches here only if no reboot happened (e.g. no GPU / drivers already installed) install_status = {"running": False, "done": True, "error": None} except Exception as e: install_status = {"running": False, "done": False, "error": str(e)} # ─── HTML UI ────────────────────────────────────────────── HTML = r""" Cezen AI Suite — Server Setup
Server Setup Wizard
1 · Network
2 · Select Tier
3 · AI Tools
4 · Install

Network Configuration

Choose how this server gets its IP address. You can change this later.

Server will get an IP automatically from your network. Current IP: detecting...
""" # ─── HTTP Handler ───────────────────────────────────────── class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass # suppress access log def send_json(self, data, code=200): body = json.dumps(data).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", len(body)) self.end_headers() self.wfile.write(body) def do_GET(self): path = urlparse(self.path).path if path == "/" or path == "/index.html": body = HTML.encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", len(body)) self.end_headers() self.wfile.write(body) elif path == "/api/status": self.send_json({ "ip": get_ip(), "interfaces": get_interfaces(), "setup_done": os.path.exists(SETUP_DONE_FILE), "install_status": install_status, }) elif path == "/api/progress": self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.end_headers() try: with open(INSTALL_LOG, "r") as f: f.seek(0, 2) # seek to end while install_status["running"]: line = f.readline() if line: msg = f"data: {line.rstrip()}\n\n" self.wfile.write(msg.encode()) self.wfile.flush() else: time.sleep(0.5) # Stream remaining lines after finish for line in f: msg = f"data: {line.rstrip()}\n\n" self.wfile.write(msg.encode()) self.wfile.write(b"data: === INSTALL COMPLETE ===\n\n") self.wfile.flush() except (BrokenPipeError, ConnectionResetError): pass else: self.send_response(404) self.end_headers() def do_POST(self): path = urlparse(self.path).path length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) if length else {} if path == "/api/network": try: if body.get("mode") == "static": ifaces = get_interfaces() iface = ifaces[0] if ifaces else "eth0" apply_static_ip(iface, body["ip"], body["prefix"], body["gateway"], body["dns"]) self.send_json({"ok": True, "ip": get_ip()}) except Exception as e: self.send_json({"ok": False, "error": str(e)}, 500) elif path == "/api/install": global install_proc tier = body.get("tier", "entry") skip = body.get("skip_tools", []) if not install_status["running"]: t = threading.Thread(target=run_install, args=(tier, skip), daemon=True) t.start() self.send_json({"ok": True}) else: self.send_json({"ok": False, "error": "Install already running"}) else: self.send_response(404) self.end_headers() # ─── Main ───────────────────────────────────────────────── if __name__ == "__main__": # Ensure log file exists open(INSTALL_LOG, "a").close() ip = get_ip() print(f"\n{'='*50}") print(f" Cezen AI Suite — Setup Server") print(f" Open in browser: http://{ip}") print(f" Or: http://cezenai.local") print(f"{'='*50}\n") server = HTTPServer(("0.0.0.0", 80), Handler) server.serve_forever()