#!/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
CEZEN AI SUITE
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...
Select AI Package Tier
Choose the tier that matches your GPU hardware.
Entry
3× NVIDIA L40S
Up to 20 concurrent users
Mid
3× RTX Pro 6000
Up to 50 concurrent users
Advanced
8× HGX H200
200+ concurrent users
Select AI Tools
Toggle the components you want installed. Recommended defaults are pre-selected.
Review Configuration
Confirm your settings before installation begins.
⏱ Installation takes 20–40 minutes. The server will reboot once to load NVIDIA drivers, then continue automatically.
Installing Cezen AI Suite...
Starting...
✅
Installation Complete!
Your Cezen AI Suite is ready.
Open WebUI:3001
JupyterLab:8888
MLflow:5000
MinIO:9000
Grafana:3000
"""
# ─── 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()