AQIT 0.1.0
Loading...
Searching...
No Matches
setup_cli.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6"""aquin setup machine — prepare this host as a (remote) compute box for desktop/CLI."""
7
8from __future__ import annotations
9
10import json
11import os
12import platform
13import socket
14import subprocess
15import sys
16import time
17from datetime import datetime, timezone
18from pathlib import Path
19from typing import Any
20
21
22def _utc_now() -> str:
23 return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
24
25
26def _gpu_summary() -> dict[str, Any]:
27 out: dict[str, Any] = {"accelerator": "cpu", "detail": None}
28 try:
29 import torch
31 if torch.cuda.is_available():
32 out["accelerator"] = "cuda"
33 out["detail"] = torch.cuda.get_device_name(0)
34 out["device_count"] = torch.cuda.device_count()
35 elif getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
36 out["accelerator"] = "mps"
37 out["detail"] = "Apple Metal"
38 else:
39 out["accelerator"] = "cpu"
40 out["detail"] = "torch installed, no CUDA/MPS"
41 out["torch"] = getattr(torch, "__version__", "?")
42 except Exception as exc:
43 out["accelerator"] = "unknown"
44 out["detail"] = f"torch unavailable: {exc}"
45 return out
46
47
48def _sshd_listening() -> dict[str, Any]:
49 """Best-effort: is something listening on port 22? (OpenSSH usually)."""
50 result: dict[str, Any] = {"port_22_open": None, "notes": []}
51 try:
52 with socket.create_connection(("127.0.0.1", 22), timeout=0.4):
53 result["port_22_open"] = True
54 except OSError:
55 result["port_22_open"] = False
56 result["notes"].append(
57 "Nothing on localhost:22 — install/start OpenSSH (sshd) if this box should accept SSH."
58 )
59
60 # Optional service name hints
61 for cmd in (
62 ["systemctl", "is-active", "ssh"],
63 ["systemctl", "is-active", "sshd"],
64 ["service", "ssh", "status"],
65 ):
66 try:
67 r = subprocess.run(
68 cmd,
69 capture_output=True,
70 text=True,
71 timeout=2,
72 )
73 text = (r.stdout or r.stderr or "").strip().lower()
74 if "active" in text or r.returncode == 0:
75 result["service_hint"] = " ".join(cmd)
76 break
77 except Exception:
78 continue
79 return result
80
81
82def _local_ips() -> list[str]:
83 ips: list[str] = []
84 try:
85 hostname = socket.gethostname()
86 for info in socket.getaddrinfo(hostname, None):
87 ip = info[4][0]
88 if ip and ip not in ips and not ip.startswith("127."):
89 ips.append(ip)
90 except Exception:
91 pass
92 return ips[:8]
93
94
95def _ensure_aquin_home() -> Path:
96 home = Path(os.environ.get("AQUIN_HOME") or (Path.home() / ".aquin"))
97 home.mkdir(parents=True, exist_ok=True)
98 return home
100
101def _start_daemon(port: int, wait_s: float = 40.0) -> dict[str, Any]:
102 from aquin.engine import model_daemon
103 from aquin.engine.engine_info import resolve_engine_port
104
105 if port:
106 os.environ["AQUIN_ENGINE_PORT"] = str(port)
107 effective = port or resolve_engine_port()
108 already = model_daemon.is_running()
109 if already:
110 h = model_daemon.health() or {}
111 return {
112 "ok": True,
113 "already": True,
114 "port": effective,
115 "health": h,
116 }
117 ok = model_daemon.ensure_running(wait_seconds=wait_s)
118 h = model_daemon.health() if ok else None
119 return {
120 "ok": bool(ok),
121 "already": False,
122 "port": effective,
123 "health": h or {},
124 "error": None if ok else "daemon did not become healthy (see ~/.aquin/daemon.log)",
125 }
126
127
128def _write_machine_record(payload: dict[str, Any]) -> Path:
129 home = _ensure_aquin_home()
130 path = home / "machine-setup.json"
131 path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
132 return path
133
134
135def _tunnel_block(port: int, user: str, host: str) -> str:
136 target = f"{user}@{host}" if user and host else "USER@HOST"
137 return "\n".join(
138 [
139 "# ── Desktop / laptop connection ─────────────────────────────────",
140 "# On your laptop (keep tunnel open):",
141 f"ssh -N -L {port}:127.0.0.1:{port} {target}",
142 "# or from monorepo:",
143 f"./ide/app/scripts/ssh-engine-tunnel.sh {target}",
144 "",
145 "# Then start Aquin desktop with remote engine:",
146 f"export AQUIN_ENGINE_REMOTE=1",
147 f"export AQUIN_ENGINE_HOST=127.0.0.1",
148 f"export AQUIN_ENGINE_PORT={port}",
149 "cd ide/app && npm start",
150 "",
151 "# Engine stays on *this* machine (127.0.0.1 only). Tunnel makes it look local.",
152 "# Optional: aquin load model <id> here to put a model in VRAM.",
153 ]
154 )
155
156
157def cmd_setup(args: list[str]) -> None:
158 """
159 aquin setup machine — prepare this host for local or remote (desktop tunnel) compute.
160 """
161 if not args or args[0] in ("--help", "-h", "help"):
162 print("Usage: aquin setup machine [options]")
163 print("")
164 print(" Probe GPU + SSH readiness, ensure ~/.aquin, start the model daemon")
165 print(" (localhost only), and print SSH tunnel + desktop env for remote GPU.")
166 print("")
167 print("Options:")
168 print(" --port N Daemon port (default: 17832 / AQUIN_ENGINE_PORT)")
169 print(" --no-daemon Do not start the model daemon")
170 print(" --check Report only (no write / no start)")
171 print(" --json Machine-readable summary on stdout")
172 print(" --ssh-user U Hint for tunnel command (default: $USER)")
173 print(" --ssh-host H Hint for tunnel command (default: hostname)")
174 return
175
176 if args[0] != "machine":
177 print(f"Unknown setup target: {args[0]}")
178 print("Usage: aquin setup machine")
179 sys.exit(1)
180
181 rest = args[1:]
182 port = 0
183 no_daemon = False
184 check_only = False
185 as_json = False
186 ssh_user = (os.environ.get("USER") or os.environ.get("LOGNAME") or "").strip() or "user"
187 ssh_host = socket.gethostname()
188
189 i = 0
190 while i < len(rest):
191 a = rest[i]
192 if a in ("--help", "-h", "help"):
193 cmd_setup([])
194 return
195 if a == "--port" and i + 1 < len(rest):
196 port = int(rest[i + 1])
197 i += 2
198 continue
199 if a.startswith("--port="):
200 port = int(a.split("=", 1)[1])
201 i += 1
202 continue
203 if a == "--no-daemon":
204 no_daemon = True
205 i += 1
206 continue
207 if a == "--check":
208 check_only = True
209 i += 1
210 continue
211 if a == "--json":
212 as_json = True
213 i += 1
214 continue
215 if a == "--ssh-user" and i + 1 < len(rest):
216 ssh_user = rest[i + 1]
217 i += 2
218 continue
219 if a == "--ssh-host" and i + 1 < len(rest):
220 ssh_host = rest[i + 1]
221 i += 2
222 continue
223 print(f"Unknown flag: {a}", file=sys.stderr)
224 sys.exit(1)
225
226 from aquin.engine.engine_info import DEFAULT_ENGINE_PORT, resolve_engine_port
227
228 if not port:
229 port = resolve_engine_port() or DEFAULT_ENGINE_PORT
230
231 summary: dict[str, Any] = {
232 "command": "setup machine",
233 "check_only": check_only,
234 "when": _utc_now(),
235 "hostname": socket.gethostname(),
236 "platform": {
237 "system": platform.system(),
238 "release": platform.release(),
239 "machine": platform.machine(),
240 "python": sys.version.split()[0],
241 "executable": sys.executable,
242 },
243 "gpu": _gpu_summary(),
244 "ssh": _sshd_listening(),
245 "network": {"local_ips": _local_ips()},
246 "port": port,
247 "daemon": None,
248 "ok": True,
249 "next": {},
250 }
251
252 try:
253 import aquin # noqa: F401
254
255 summary["aquin_import"] = "ok"
256 except Exception as exc:
257 summary["aquin_import"] = f"failed: {exc}"
258 summary["ok"] = False
259
260 if not no_daemon and not check_only and summary.get("aquin_import") == "ok":
261 daemon_info = _start_daemon(port)
262 summary["daemon"] = daemon_info
263 if not daemon_info.get("ok"):
264 summary["ok"] = False
265 elif not no_daemon:
266 # check path: probe only
267 try:
268 from aquin.engine import model_daemon
269
270 h = model_daemon.health()
271 summary["daemon"] = {
272 "ok": h is not None,
273 "already": h is not None,
274 "port": port,
275 "health": h or {},
276 }
277 except Exception as exc:
278 summary["daemon"] = {"ok": False, "error": str(exc), "port": port}
279
280 tunnel = _tunnel_block(port, ssh_user, ssh_host)
281 summary["next"] = {
282 "ssh_target": f"{ssh_user}@{ssh_host}",
283 "tunnel": f"ssh -N -L {port}:127.0.0.1:{port} {ssh_user}@{ssh_host}",
284 "desktop_env": {
285 "AQUIN_ENGINE_REMOTE": "1",
286 "AQUIN_ENGINE_HOST": "127.0.0.1",
287 "AQUIN_ENGINE_PORT": str(port),
288 },
289 "load_model": "aquin load model <id>",
290 }
291
292 if not check_only:
293 try:
295 {
296 "setup_at": summary["when"],
297 "hostname": summary["hostname"],
298 "port": port,
299 "ssh_target": summary["next"]["ssh_target"],
300 "gpu": summary["gpu"],
301 "daemon_ok": (summary.get("daemon") or {}).get("ok"),
302 }
303 )
304 summary["wrote"] = str(path)
305 except Exception as exc:
306 summary["wrote_error"] = str(exc)
307
308 if as_json:
309 print(json.dumps(summary, indent=2, ensure_ascii=False))
310 sys.exit(0 if summary.get("ok") else 1)
311
312 # Human output — agent-friendly, greppable markers
313 print("[setup machine] host probes")
314 print(f" hostname : {summary['hostname']}")
315 print(f" platform : {summary['platform']['system']} {summary['platform']['machine']}")
316 print(f" python : {summary['platform']['python']} ({summary['platform']['executable']})")
317 print(f" aquin : {summary.get('aquin_import')}")
318 gpu = summary["gpu"]
319 print(f" accelerator: {gpu.get('accelerator')} {gpu.get('detail') or ''}".rstrip())
320 ssh = summary["ssh"]
321 print(
322 f" ssh :22 : "
323 f"{'open' if ssh.get('port_22_open') else 'closed/unavailable' if ssh.get('port_22_open') is False else 'unknown'}"
324 )
325 for n in ssh.get("notes") or []:
326 print(f" note: {n}")
327
328 d = summary.get("daemon") or {}
329 if no_daemon:
330 print("[setup machine] daemon: skipped (--no-daemon)")
331 elif check_only:
332 print(f"[setup machine] daemon check: ok={d.get('ok')} port={d.get('port', port)}")
333 else:
334 if d.get("ok"):
335 state = "already running" if d.get("already") else "started"
336 print(f"[setup machine] daemon: {state} on 127.0.0.1:{d.get('port', port)}")
337 h = d.get("health") or {}
338 if h.get("model_id"):
339 print(f" model_id : {h.get('model_id')}")
340 else:
341 print(f"[setup machine] daemon: FAILED — {d.get('error') or 'unknown'}", file=sys.stderr)
342
343 if summary.get("wrote"):
344 print(f"[setup machine] wrote {summary['wrote']}")
345
346 print("")
347 print(tunnel)
348 print("")
349 if summary.get("ok"):
350 print("[setup machine] OK — engine ready for SSH tunnel from your laptop.")
351 else:
352 print("[setup machine] completed with issues (see above).", file=sys.stderr)
353 sys.exit(1)
Path _write_machine_record(dict[str, Any] payload)
Definition setup_cli.py:132
None cmd_setup(list[str] args)
Definition setup_cli.py:161
dict[str, Any] _sshd_listening()
Definition setup_cli.py:52
dict[str, Any] _gpu_summary()
Definition setup_cli.py:30
dict[str, Any] _start_daemon(int port, float wait_s=40.0)
Definition setup_cli.py:105
list[str] _local_ips()
Definition setup_cli.py:86
str _tunnel_block(int port, str user, str host)
Definition setup_cli.py:139
Path _ensure_aquin_home()
Definition setup_cli.py:99