Add signed licensing enforcement and ISO notes

This commit is contained in:
Jino Jose committed 2026-07-08 10:47:49 +05:30
1 parent dee4486826
commit d645249a13
14 files changed
+1526 -103

No files matched your search

+174 -27
View File
@@ -3,15 +3,28 @@
Nexus One AI — 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
import os, json, subprocess, threading, time, socket, ipaddress, sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
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"
SCRIPT_ROOT = Path(__file__).resolve().parents[2]
SCRIPTS_DIR = Path(AIPACKAGE_DIR) / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR if SCRIPTS_DIR.exists() else SCRIPT_ROOT / "scripts"))
from cezen_license import build_tier_options, evaluate_license, evaluate_override, normalize_tier, read_json_file, write_json_file
install_proc = None
install_status = {"running": False, "done": False, "error": None}
FEASIBILITY_SCRIPT = f"{AIPACKAGE_DIR}/scripts/cezen-feasibility.sh"
FEASIBILITY_JSON = "/opt/cezen/feasibility.json"
LICENSE_JSON = "/opt/cezen/license.json"
OVERRIDE_JSON = "/opt/cezen/license.override.json"
INSTALL_RECORD_JSON = "/opt/cezen/install-record.json"
PUBLIC_KEY_PATH = os.environ.get("CEZEN_LICENSE_PUBLIC_KEY", f"{AIPACKAGE_DIR}/autoinstall/keys/cezen-license-public.pem")
# ─── Helpers ──────────────────────────────────────────────
def get_ip():
@@ -91,35 +104,84 @@ def apply_static_ip(iface, ip, prefix, gateway, dns):
subprocess.run(["netplan", "apply"], capture_output=True)
time.sleep(3)
def write_license_file(license_data, tier):
def save_license_artifacts(license_data):
os.makedirs("/opt/cezen", exist_ok=True)
for target in (LICENSE_JSON, OVERRIDE_JSON):
try:
os.remove(target)
except FileNotFoundError:
pass
raw_license = (license_data.get("license_json") or "").strip()
raw_override = (license_data.get("override_json") or "").strip()
if raw_license:
payload = json.loads(raw_license)
write_json_file(LICENSE_JSON, payload)
subprocess.run(["chown", "root:cezen", LICENSE_JSON], check=False)
os.chmod(LICENSE_JSON, 0o640)
if raw_override:
payload = json.loads(raw_override)
write_json_file(OVERRIDE_JSON, payload)
subprocess.run(["chown", "root:cezen", OVERRIDE_JSON], check=False)
os.chmod(OVERRIDE_JSON, 0o640)
def ensure_feasibility():
os.makedirs("/opt/cezen", exist_ok=True)
if os.path.exists(FEASIBILITY_SCRIPT):
subprocess.run(["bash", FEASIBILITY_SCRIPT, FEASIBILITY_JSON], check=False)
return read_json_file(FEASIBILITY_JSON)
def evaluate_install_constraints(license_data):
save_license_artifacts(license_data or {})
feasibility = ensure_feasibility()
hardware_tier = normalize_tier(((feasibility.get("recommendation") or {}).get("recommended_tier")), "basic")
license_record = read_json_file(LICENSE_JSON)
override_record = read_json_file(OVERRIDE_JSON)
license_eval = evaluate_license(license_record, PUBLIC_KEY_PATH)
override_eval = evaluate_override(override_record, PUBLIC_KEY_PATH)
return {
"license": license_eval,
"override": override_eval,
"hardware": {
"recommended_tier": hardware_tier,
"recommended_profile": (feasibility.get("recommendation") or {}).get("recommended_profile", ""),
"estimated_concurrent_users": (feasibility.get("recommendation") or {}).get("estimated_concurrent_users", ""),
"notes": (feasibility.get("recommendation") or {}).get("notes", []),
},
"tier_options": build_tier_options(license_eval, hardware_tier, override_eval),
}
def write_install_record(license_data, tier, skip_tools, preview):
payload = {
"schema": "cezen.license.v1",
"schema": "cezen.install_record.v1",
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"customer_name": (license_data.get("customer_name") or "").strip(),
"customer_id": (license_data.get("customer_id") or "").strip(),
"contact_email": (license_data.get("contact_email") or "").strip(),
"license_key": (license_data.get("license_key") or "").strip(),
"tier": tier,
"support_until": (license_data.get("support_until") or "").strip(),
"install_type": (license_data.get("install_type") or "customer").strip(),
"issued_by": "Cezen",
"captured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"signature_status": "unsigned-field-install",
"selected_tier": tier,
"provisioned_tier": tier,
"licensed_tier": ((preview.get("license") or {}).get("allowed_tier") or "basic"),
"license_status": ((preview.get("license") or {}).get("status") or "missing"),
"hardware_recommended_tier": ((preview.get("hardware") or {}).get("recommended_tier") or "starter"),
"hardware_recommended_profile": ((preview.get("hardware") or {}).get("recommended_profile") or "core"),
"install_type": ((preview.get("license") or {}).get("install_type") or "field-staging"),
"override_active": bool((preview.get("override") or {}).get("allow_hardware_mismatch")),
"skip_roles": skip_tools or [],
"components": {k: k not in set(skip_tools or []) for k in ["ollama", "jupyterlab", "chromadb", "vllm", "mlflow", "minio", "monitoring", "k3s"]},
}
with open("/opt/cezen/license.json", "w") as f:
json.dump(payload, f, indent=2)
subprocess.run(["chown", "root:cezen", "/opt/cezen/license.json"], check=False)
os.chmod("/opt/cezen/license.json", 0o640)
return payload
write_json_file(INSTALL_RECORD_JSON, payload)
subprocess.run(["chown", "root:cezen", INSTALL_RECORD_JSON], check=False)
os.chmod(INSTALL_RECORD_JSON, 0o640)
def run_install(tier, skip_tools, license_data=None):
def run_install(tier, skip_tools, license_data=None, preview=None):
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 ""
write_license_file(license_data or {}, tier)
save_license_artifacts(license_data or {})
write_install_record(license_data or {}, tier, skip_tools or [], preview or {})
with open("/opt/cezen/install.conf", "w") as f:
f.write(f"TIER={tier}\nSKIP_ROLES={skip_str}\n")
@@ -187,9 +249,13 @@ HTML = r"""<!DOCTYPE html>
transition: all .2s; text-align: center; }
.tier-card:hover { border-color: var(--teal2); background: #F0FDFA; }
.tier-card.selected { border-color: var(--teal); background: #CCFBF1; }
.tier-card.disabled { cursor: not-allowed; opacity: .58; background: #F8FAFC; }
.tier-card.disabled:hover { border-color: #E2E8F0; background: #F8FAFC; }
.tier-card.override { border-style: dashed; }
.tier-card .tier-name { font-size: 16px; font-weight: 700; color: var(--navy); margin-bottom: 4px; }
.tier-card .tier-gpu { font-size: 12px; color: var(--teal); font-weight: 600; margin-bottom: 8px; }
.tier-card .tier-users { font-size: 12px; color: var(--muted); }
.tier-card .tier-note { font-size: 11px; color: var(--red); margin-top: 8px; min-height: 28px; }
/* Tool toggles */
.tool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@@ -217,9 +283,10 @@ HTML = r"""<!DOCTYPE html>
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; color: var(--navy); margin-bottom: 6px; }
.form-group input { width: 100%; padding: 10px 14px; border: 1.5px solid #CBD5E1; border-radius: 8px;
.form-group input, .form-group textarea { width: 100%; padding: 10px 14px; border: 1.5px solid #CBD5E1; border-radius: 8px;
font-size: 14px; outline: none; transition: border .15s; }
.form-group input:focus { border-color: var(--teal); }
.form-group textarea { min-height: 110px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
.form-group input:focus, .form-group textarea:focus { border-color: var(--teal); }
.static-fields { display: none; }
.static-fields.show { display: block; }
.ip-row { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; }
@@ -338,7 +405,7 @@ HTML = r"""<!DOCTYPE html>
<div id="step-2" class="hidden">
<div class="card">
<h2>License & Customer Details</h2>
<p class="desc">Enter the customer/license details for this installation. Leave the license key blank for evaluation or PSU field staging.</p>
<p class="desc">Enter the local customer record and paste the signed license JSON. Leave license blank for field staging.</p>
<div class="form-group">
<label>Customer / Organisation Name</label>
<input type="text" id="lic-customer-name" placeholder="e.g. Bharat Electronics Limited">
@@ -352,17 +419,25 @@ HTML = r"""<!DOCTYPE html>
<input type="email" id="lic-contact-email" placeholder="admin@example.gov.in">
</div>
<div class="form-group">
<label>License Key / Activation Code</label>
<input type="text" id="lic-license-key" placeholder="Optional for offline field install">
<label>Signed License JSON</label>
<textarea id="lic-license-json" placeholder='Paste the signed cezen.license.v2 JSON here'></textarea>
</div>
<div class="form-group">
<label>Cezen Override JSON</label>
<textarea id="lic-override-json" placeholder='Optional: signed cezen.override.v1 JSON for hardware mismatch'></textarea>
</div>
<div class="form-group">
<label>Support Until</label>
<input type="date" id="lic-support-until">
</div>
<div class="form-group">
<label>License Preview</label>
<div id="license-preview" style="padding:12px 14px;border:1px solid #CBD5E1;border-radius:8px;background:#F8FAFC;color:var(--muted);font-size:13px">No license evaluated yet.</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="goStep(1)">← Back</button>
<button class="btn btn-primary" onclick="goStep(3)">Next →</button>
<button class="btn btn-primary" onclick="prepareTierStep()">Next →</button>
</div>
</div>
@@ -376,21 +451,25 @@ HTML = r"""<!DOCTYPE html>
<div class="tier-name">Starter</div>
<div class="tier-gpu">1× RTX 5090 / 32GB VRAM</div>
<div class="tier-users">Small team deployment</div>
<div class="tier-note" id="tier-note-starter"></div>
</div>
<div class="tier-card" id="tier-basic" onclick="selectTier('basic')">
<div class="tier-name">Entry</div>
<div class="tier-gpu">1× NVIDIA RTX Pro 6000 (96GB)</div>
<div class="tier-users">Up to 20 concurrent users</div>
<div class="tier-note" id="tier-note-basic"></div>
</div>
<div class="tier-card" id="tier-pro" onclick="selectTier('pro')">
<div class="tier-name">Pro</div>
<div class="tier-gpu">2× RTX 5090 / RTX Pro class</div>
<div class="tier-users">Up to 100 concurrent users</div>
<div class="tier-note" id="tier-note-pro"></div>
</div>
<div class="tier-card" id="tier-max" onclick="selectTier('max')">
<div class="tier-name">Max</div>
<div class="tier-gpu">4–8× H100/H200/A100 class</div>
<div class="tier-users">200+ concurrent users</div>
<div class="tier-note" id="tier-note-max"></div>
</div>
</div>
</div>
@@ -461,6 +540,7 @@ HTML = r"""<!DOCTYPE html>
// ── State ──────────────────────────────────────────────────
let netMode = 'dhcp';
let selectedTier = 'basic';
let tierPreview = null;
let tools = {
ollama: { name: 'Ollama + Open WebUI', desc: 'LLM inference & chat', icon: '🤖', on: true },
jupyterlab: { name: 'JupyterLab', desc: 'Notebook environment', icon: '📓', on: true },
@@ -478,7 +558,7 @@ window.onload = () => {
document.getElementById('current-ip').textContent = d.ip || 'unknown';
});
renderTools();
selectTier('basic');
renderTierAvailability(null);
};
// ── Navigation ─────────────────────────────────────────────
@@ -525,7 +605,28 @@ function applyStaticIP() {
}
// ── Tier ───────────────────────────────────────────────────
function renderTierAvailability(preview) {
tierPreview = preview;
const options = {};
(preview?.tier_options || []).forEach(opt => options[opt.tier] = opt);
['starter','basic','pro','max'].forEach(tier => {
const card = document.getElementById('tier-' + tier);
const note = document.getElementById('tier-note-' + tier);
const meta = options[tier] || { state: 'disabled_by_license', reason: 'Preview license to continue.', selectable: false };
card.classList.toggle('disabled', !meta.selectable);
card.classList.toggle('override', meta.state === 'override_required');
if (!meta.selectable && selectedTier === tier) selectedTier = '';
note.textContent = meta.reason || (meta.state === 'override_required' ? 'Override active for hardware mismatch.' : '');
});
const fallback = (preview?.tier_options || []).find(opt => opt.selectable);
if (fallback && (!selectedTier || !(options[selectedTier] || {}).selectable)) {
selectTier(fallback.tier);
}
}
function selectTier(t) {
const opt = (tierPreview?.tier_options || []).find(x => x.tier === t);
if (opt && !opt.selectable) return;
selectedTier = t;
['starter','basic','pro','max'].forEach(x =>
document.getElementById('tier-'+x).classList.toggle('selected', x===t));
@@ -556,12 +657,32 @@ function collectLicense() {
customer_name: document.getElementById('lic-customer-name').value.trim(),
customer_id: document.getElementById('lic-customer-id').value.trim(),
contact_email: document.getElementById('lic-contact-email').value.trim(),
license_key: document.getElementById('lic-license-key').value.trim(),
license_json: document.getElementById('lic-license-json').value.trim(),
override_json: document.getElementById('lic-override-json').value.trim(),
support_until: document.getElementById('lic-support-until').value.trim(),
install_type: document.getElementById('lic-license-key').value.trim() ? 'licensed' : 'field-staging'
install_type: document.getElementById('lic-license-json').value.trim() ? 'licensed' : 'field-staging'
};
}
async function prepareTierStep() {
try {
const res = await api('/api/license-preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ license: collectLicense() })
});
renderTierAvailability(res);
document.getElementById('license-preview').innerHTML = `
<strong>Status:</strong> ${esc(res.license?.status || 'missing')}<br>
<strong>Allowed tier:</strong> ${esc(res.license?.allowed_tier || 'basic')}<br>
<strong>Hardware recommendation:</strong> ${esc(res.hardware?.recommended_tier || 'starter')}
`;
goStep(3);
} catch (err) {
alert('License preview failed: ' + err.message);
}
}
// ── Summary ────────────────────────────────────────────────
function renderSummary() {
const ip = netMode === 'dhcp'
@@ -573,7 +694,9 @@ function renderSummary() {
document.getElementById('summary-rows').innerHTML = `
<div class="summary-row"><span class="key">Network</span><span class="val">${ip}</span></div>
<div class="summary-row"><span class="key">Customer</span><span class="val">${license.customer_name || 'Not entered'}</span></div>
<div class="summary-row"><span class="key">License</span><span class="val">${license.license_key ? 'Provided' : 'Field staging / evaluation'}</span></div>
<div class="summary-row"><span class="key">License</span><span class="val">${license.license_json ? (tierPreview?.license?.status || 'Provided') : 'Field staging / evaluation'}</span></div>
<div class="summary-row"><span class="key">Allowed Tier</span><span class="val">${tierPreview?.license?.allowed_tier || 'basic'}</span></div>
<div class="summary-row"><span class="key">Hardware Tier</span><span class="val">${tierPreview?.hardware?.recommended_tier || 'starter'}</span></div>
<div class="summary-row"><span class="key">Tier</span><span class="val">${selectedTier.charAt(0).toUpperCase()+selectedTier.slice(1)}</span></div>
<div class="summary-row"><span class="key">Tools</span><span class="val" style="font-size:13px;text-align:right;max-width:60%">${onTools}</span></div>
${offTools.length ? `<div class="summary-row"><span class="key">Skipped</span><span class="val" style="color:var(--muted);font-size:13px">${offTools.map(([,v])=>v.name).join(', ')}</span></div>` : ''}
@@ -591,6 +714,14 @@ function startInstall() {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ tier: selectedTier, skip_tools: skip, license: collectLicense() })
}).then(async r => {
const data = await r.json();
if (!r.ok || !data.ok) throw new Error(data.error || 'Install start failed');
}).catch(err => {
alert(err.message);
document.getElementById('install-btn-row').classList.remove('hidden');
document.getElementById('summary-card').classList.remove('hidden');
document.getElementById('progress-wrap').classList.remove('show');
});
streamLog();
@@ -780,13 +911,29 @@ class Handler(BaseHTTPRequestHandler):
except Exception as e:
self.send_json({"ok": False, "error": str(e)}, 500)
elif path == "/api/license-preview":
try:
preview = evaluate_install_constraints(body.get("license", {}) or {})
self.send_json(preview)
except Exception as e:
self.send_json({"ok": False, "error": str(e)}, 400)
elif path == "/api/install":
global install_proc
tier = body.get("tier", "basic")
skip = body.get("skip_tools", [])
license_data = body.get("license", {})
try:
preview = evaluate_install_constraints(license_data or {})
except Exception as e:
self.send_json({"ok": False, "error": f"License validation failed: {e}"}, 400)
return
selected = next((opt for opt in preview.get("tier_options", []) if opt.get("tier") == tier), None)
if not selected or not selected.get("selectable"):
self.send_json({"ok": False, "error": f"Tier '{tier}' is not allowed for this license/hardware."}, 400)
return
if not install_status["running"]:
t = threading.Thread(target=run_install, args=(tier, skip, license_data), daemon=True)
t = threading.Thread(target=run_install, args=(tier, skip, license_data, preview), daemon=True)
t.start()
self.send_json({"ok": True})
else: