Update ISO installer tiers and backend deployment
This commit is contained in:
1 parent
79784a6743
commit
56668f7bdc
38 files changed
+7206
-175
No files matched your search
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus One AI backup/restore helper.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash scripts/cezen-backup.sh backup
|
||||
# sudo bash scripts/cezen-backup.sh list
|
||||
# sudo bash scripts/cezen-backup.sh restore /opt/cezen/backups/cezen-backup-YYYYmmdd-HHMMSS.zip
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="${1:-backup}"
|
||||
TARGET="${2:-}"
|
||||
DATA_DIR="${CEZEN_DATA:-/opt/cezen/data}"
|
||||
BACKUP_DIR="${CEZEN_BACKUP_DIR:-/opt/cezen/backups}"
|
||||
|
||||
python3 - "$ACTION" "$TARGET" "$DATA_DIR" "$BACKUP_DIR" <<'PY'
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
action, target, data_dir, backup_dir = sys.argv[1:5]
|
||||
data_dir = Path(data_dir)
|
||||
backup_dir = Path(backup_dir)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def now_tag():
|
||||
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
def iso_now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def write_backup(dest):
|
||||
manifest = {
|
||||
"schema": "cezen.backup_manifest.v1",
|
||||
"created_at": iso_now(),
|
||||
"data_dir": str(data_dir),
|
||||
"source": "cezen-backup.sh",
|
||||
}
|
||||
with zipfile.ZipFile(dest, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
|
||||
if data_dir.exists():
|
||||
for path in data_dir.rglob("*"):
|
||||
if path.is_file():
|
||||
zf.write(path, path.relative_to(data_dir).as_posix())
|
||||
|
||||
def restore(src):
|
||||
src = Path(src)
|
||||
if not src.exists():
|
||||
raise SystemExit(f"Backup not found: {src}")
|
||||
safety = backup_dir / f"pre-restore-{now_tag()}.zip"
|
||||
write_backup(safety)
|
||||
root = data_dir.resolve()
|
||||
with zipfile.ZipFile(src, "r") as zf:
|
||||
for member in zf.infolist():
|
||||
if member.filename == "manifest.json" or member.is_dir():
|
||||
continue
|
||||
target_path = (data_dir / member.filename).resolve()
|
||||
if root not in target_path.parents and target_path != root:
|
||||
raise SystemExit(f"Unsafe archive path: {member.filename}")
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zf.open(member) as source, open(target_path, "wb") as out:
|
||||
shutil.copyfileobj(source, out)
|
||||
print(json.dumps({"ok": True, "restored": str(src), "pre_restore_snapshot": str(safety)}, indent=2))
|
||||
|
||||
if action == "backup":
|
||||
dest = backup_dir / f"cezen-backup-{now_tag()}.zip"
|
||||
write_backup(dest)
|
||||
print(json.dumps({"ok": True, "backup": str(dest)}, indent=2))
|
||||
elif action == "list":
|
||||
rows = []
|
||||
for path in sorted(backup_dir.glob("cezen-backup-*.zip"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
rows.append({"name": path.name, "path": str(path), "size_bytes": path.stat().st_size})
|
||||
print(json.dumps({"backup_dir": str(backup_dir), "backups": rows}, indent=2))
|
||||
elif action == "restore":
|
||||
if not target:
|
||||
raise SystemExit("Usage: cezen-backup.sh restore /path/to/backup.zip")
|
||||
restore(target)
|
||||
else:
|
||||
raise SystemExit("Usage: cezen-backup.sh backup|list|restore [backup.zip]")
|
||||
PY
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus One AI feasibility checker.
|
||||
# Runs before installation to classify existing hardware for software-only or appliance deployments.
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${1:-/opt/cezen/feasibility.json}"
|
||||
mkdir -p "$(dirname "$OUT")" 2>/dev/null || true
|
||||
|
||||
tmp_json="$(mktemp /tmp/cezen-feasibility.XXXXXX.json)"
|
||||
|
||||
python3 - "$tmp_json" <<'PY'
|
||||
import json, os, platform, shutil, socket, subprocess, sys
|
||||
from pathlib import Path
|
||||
|
||||
out = Path(sys.argv[1])
|
||||
|
||||
def run(cmd, timeout=5):
|
||||
try:
|
||||
return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True, timeout=timeout).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def read(path, default=""):
|
||||
try:
|
||||
return Path(path).read_text(errors="replace").strip()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def gb(n):
|
||||
return round(n / (1024 ** 3), 1)
|
||||
|
||||
def os_info():
|
||||
data = {}
|
||||
for line in read("/etc/os-release").splitlines():
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
data[k] = v.strip('"')
|
||||
return {
|
||||
"name": data.get("PRETTY_NAME") or platform.platform(),
|
||||
"id": data.get("ID", ""),
|
||||
"version_id": data.get("VERSION_ID", ""),
|
||||
"kernel": platform.release(),
|
||||
}
|
||||
|
||||
def cpu_info():
|
||||
model = ""
|
||||
for line in read("/proc/cpuinfo").splitlines():
|
||||
if line.lower().startswith("model name"):
|
||||
model = line.split(":", 1)[1].strip()
|
||||
break
|
||||
return {"model": model or platform.processor(), "cores": os.cpu_count() or 0}
|
||||
|
||||
def mem_gb():
|
||||
for line in read("/proc/meminfo").splitlines():
|
||||
if line.startswith("MemTotal:"):
|
||||
return round(int(line.split()[1]) / 1024 / 1024, 1)
|
||||
out = run(["sysctl", "-n", "hw.memsize"])
|
||||
if out.isdigit():
|
||||
return gb(int(out))
|
||||
try:
|
||||
pages = os.sysconf("SC_PHYS_PAGES")
|
||||
page_size = os.sysconf("SC_PAGE_SIZE")
|
||||
if pages and page_size:
|
||||
return gb(int(pages) * int(page_size))
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def disk_info():
|
||||
usage = shutil.disk_usage("/")
|
||||
return {"total_gb": gb(usage.total), "free_gb": gb(usage.free), "used_gb": gb(usage.used)}
|
||||
|
||||
def gpu_info():
|
||||
gpus = []
|
||||
if shutil.which("nvidia-smi"):
|
||||
q = "name,memory.total,driver_version"
|
||||
out = run(["nvidia-smi", f"--query-gpu={q}", "--format=csv,noheader,nounits"])
|
||||
for row in out.splitlines():
|
||||
parts = [p.strip() for p in row.split(",")]
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
vram = round(float(parts[1]) / 1024, 1)
|
||||
except Exception:
|
||||
vram = 0
|
||||
gpus.append({"name": parts[0], "vram_gb": vram, "driver": parts[2] if len(parts) > 2 else ""})
|
||||
if not gpus:
|
||||
for vendor in Path("/sys/bus/pci/devices").glob("*/vendor"):
|
||||
if read(vendor).lower() == "0x10de":
|
||||
gpus.append({"name": "NVIDIA GPU detected (driver not ready)", "vram_gb": 0, "driver": ""})
|
||||
break
|
||||
return gpus
|
||||
|
||||
def port_open(host, port):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=1):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def tool_state():
|
||||
return {
|
||||
"docker": bool(shutil.which("docker")),
|
||||
"docker_running": bool(run(["docker", "info"], timeout=3)) if shutil.which("docker") else False,
|
||||
"ansible": bool(shutil.which("ansible-playbook")),
|
||||
"python3": bool(shutil.which("python3")),
|
||||
"curl": bool(shutil.which("curl")),
|
||||
"git": bool(shutil.which("git")),
|
||||
}
|
||||
|
||||
def recommend(mem, disk, gpus, cpu_cores):
|
||||
max_vram = max([g.get("vram_gb", 0) for g in gpus] or [0])
|
||||
gpu_count = len([g for g in gpus if g.get("vram_gb", 0) > 0])
|
||||
profile = "core"
|
||||
tier = "starter"
|
||||
concurrency = "1-3"
|
||||
notes = []
|
||||
|
||||
if max_vram >= 120 and gpu_count >= 4:
|
||||
tier, profile, concurrency = "max", "gpu-max", "100+"
|
||||
elif max_vram >= 80 and gpu_count >= 2:
|
||||
tier, profile, concurrency = "pro", "gpu-pro", "20-100"
|
||||
elif max_vram >= 48:
|
||||
tier, profile, concurrency = "basic", "gpu-standard", "5-20"
|
||||
elif max_vram >= 24:
|
||||
tier, profile, concurrency = "starter", "gpu-starter", "1-10"
|
||||
elif max_vram >= 8:
|
||||
tier, profile, concurrency = "starter", "gpu-lite", "1-5"
|
||||
notes.append("GPU is suitable for small quantized models only.")
|
||||
elif mem >= 32 and cpu_cores >= 8:
|
||||
tier, profile, concurrency = "starter", "cpu-ai", "1-3"
|
||||
notes.append("No usable NVIDIA VRAM found; local CPU inference is limited. Use cloud/external model fallback for better UX.")
|
||||
else:
|
||||
tier, profile, concurrency = "starter", "core", "1-2"
|
||||
notes.append("Hardware is best for portal, RAG management, workflows, and external/cloud model routing.")
|
||||
|
||||
if mem < 16:
|
||||
notes.append("RAM below 16 GB; avoid local model serving.")
|
||||
if disk < 100:
|
||||
notes.append("Less than 100 GB free disk; model storage and document indexing will be constrained.")
|
||||
|
||||
return {
|
||||
"recommended_tier": tier,
|
||||
"recommended_profile": profile,
|
||||
"estimated_concurrent_users": concurrency,
|
||||
"notes": notes,
|
||||
}
|
||||
|
||||
mem = mem_gb()
|
||||
disk = disk_info()
|
||||
gpus = gpu_info()
|
||||
cpu = cpu_info()
|
||||
tools = tool_state()
|
||||
rec = recommend(mem, disk["free_gb"], gpus, cpu["cores"])
|
||||
|
||||
features = {
|
||||
"portal": True,
|
||||
"users_auth": True,
|
||||
"document_intelligence": mem >= 8,
|
||||
"rag_chromadb": mem >= 16 and disk["free_gb"] >= 50,
|
||||
"ollama_cpu": mem >= 32,
|
||||
"ollama_gpu": any(g.get("vram_gb", 0) >= 8 for g in gpus),
|
||||
"vllm": any(g.get("vram_gb", 0) >= 24 for g in gpus),
|
||||
"fine_tuning_qlora": any(g.get("vram_gb", 0) >= 24 for g in gpus),
|
||||
"distributed_training": len([g for g in gpus if g.get("vram_gb", 0) >= 48]) >= 2,
|
||||
"monitoring": True,
|
||||
"software_only": True,
|
||||
"air_gapped_ready": True,
|
||||
}
|
||||
|
||||
report = {
|
||||
"schema": "cezen.feasibility.v1",
|
||||
"generated_at": run(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"]) or "",
|
||||
"host": socket.gethostname(),
|
||||
"os": os_info(),
|
||||
"cpu": cpu,
|
||||
"ram_gb": mem,
|
||||
"disk": disk,
|
||||
"gpus": gpus,
|
||||
"tools": tools,
|
||||
"ports_in_use": {str(p): port_open("127.0.0.1", p) for p in [80, 8080, 11434, 8000, 3000, 8888]},
|
||||
"recommendation": rec,
|
||||
"features": features,
|
||||
}
|
||||
out.write_text(json.dumps(report, indent=2))
|
||||
PY
|
||||
|
||||
if ! cp "$tmp_json" "$OUT" 2>/dev/null; then
|
||||
OUT="./feasibility.json"
|
||||
cp "$tmp_json" "$OUT"
|
||||
fi
|
||||
rm -f "$tmp_json"
|
||||
|
||||
python3 - "$OUT" <<'PY'
|
||||
import json, sys
|
||||
p = sys.argv[1]
|
||||
d = json.load(open(p))
|
||||
r = d["recommendation"]
|
||||
print("")
|
||||
print("Nexus One AI Feasibility Report")
|
||||
print("--------------------------------")
|
||||
print(f"Host: {d['host']}")
|
||||
print(f"OS: {d['os']['name']}")
|
||||
print(f"CPU: {d['cpu']['cores']} cores | RAM: {d['ram_gb']} GB | Free disk: {d['disk']['free_gb']} GB")
|
||||
if d["gpus"]:
|
||||
print("GPU: " + "; ".join(f"{g['name']} ({g.get('vram_gb', 0)} GB VRAM)" for g in d["gpus"]))
|
||||
else:
|
||||
print("GPU: none detected")
|
||||
print("")
|
||||
print(f"Recommended tier: {r['recommended_tier'].upper()}")
|
||||
print(f"Recommended profile: {r['recommended_profile']}")
|
||||
print(f"Estimated concurrency: {r['estimated_concurrent_users']} users")
|
||||
if r["notes"]:
|
||||
print("Notes:")
|
||||
for n in r["notes"]:
|
||||
print(f" - {n}")
|
||||
print("")
|
||||
print(f"JSON report: {p}")
|
||||
PY
|
||||
Reference in new issue
Block a user