File size: 7,023 Bytes
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656439d
 
 
 
 
 
 
 
 
 
 
 
 
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env bash
# Bring up the Auralynq stack (rootless Podman, no sudo). See ADR-0012/0013/0014.
#
# Networking: this host's CNI generates conflists with cniVersion 1.0.0, but the
# installed `firewall` plugin only supports up to 0.4.0 β€” so the network silently
# loses container DNS. The default `podman` network also ships without the
# `dnsname` plugin. We fix BOTH without sudo by rewriting the relevant conflist(s)
# under ~/.config/cni/net.d to cniVersion 0.4.0 + ensuring the dnsname plugin is
# present. With DNS working, services reach peers by container_name
# (auralynq-qdrant / auralynq-api / auralynq-web) β€” see ADR-0014.
set -euo pipefail
cd "$(dirname "$0")/.."

COMPOSE="$(./scripts/check_container_runtime.sh)"
CF="compose.yml"
CNI_DIR="${HOME}/.config/cni/net.d"

# Patch a CNI conflist in place: pin cniVersion 0.4.0 and append dnsname if absent.
patch_conflist() {
  local f="$1"
  [ -f "$f" ] || return 0
  python3 - "$f" <<'PY'
import json, sys
p = sys.argv[1]
d = json.load(open(p))
changed = False
if d.get("cniVersion") != "0.4.0":
    d["cniVersion"] = "0.4.0"; changed = True
types = [pl.get("type") for pl in d.get("plugins", [])]
if "dnsname" not in types:
    d.setdefault("plugins", []).append(
        {"type": "dnsname", "domainName": "dns.podman", "capabilities": {"aliases": True}})
    changed = True
if changed:
    json.dump(d, open(p, "w"), indent=2)
    print(f"  patched {p} -> 0.4.0 + dnsname")
PY
}

echo "β†’ ensuring rootless CNI networks have DNS (no sudo)…"
# The default podman network (used by podman-compose) + any project network.
shopt -s nullglob
for f in "${CNI_DIR}"/87-podman.conflist "${CNI_DIR}"/*podman*.conflist "${CNI_DIR}"/auralynq*.conflist; do
  patch_conflist "$f"
done
shopt -u nullglob

# Prefer an exported env var (e.g. from scripts/run_local.sh) over the .env file,
# matching podman-compose's own precedence so the printed URL is accurate.
bind_internal="${AURALYNQ_BIND_INTERNAL:-$(grep -E '^AURALYNQ_BIND_INTERNAL=' .env 2>/dev/null | cut -d= -f2)}"; bind_internal="${bind_internal:-127.0.0.1}"
https_port="${AURALYNQ_HTTPS_PORT:-$(grep -E '^AURALYNQ_HTTPS_PORT=' .env 2>/dev/null | cut -d= -f2)}"; https_port="${https_port:-8443}"

# ── Host Ollama reachability (rootless) ───────────────────────────────────────
# Containers on the rootless compose bridge cannot reach host loopback services,
# and Podman auto-injects host.containers.internal pointing at the bridge gateway
# (10.88.0.1) β€” which is NOT the host in rootless mode. If the user runs Ollama
# on the host, route the containers to the host's real LAN IP instead (Ollama
# binds 0.0.0.0, so the bridge can reach it there). Without this the API silently
# degrades to the extractive LLM fallback even when a local GPU model is present.
existing_base="${AURALYNQ_LLM__BASE_URL:-$(grep -E '^AURALYNQ_LLM__BASE_URL=' .env 2>/dev/null | cut -d= -f2- || true)}"
if [ -z "${existing_base}" ]; then
  host_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1 || true)"
  if [ -n "${host_ip}" ] && curl -fsS -m 2 "http://${host_ip}:11434/api/tags" >/dev/null 2>&1; then
    export AURALYNQ_LLM__BASE_URL="http://${host_ip}:11434"
    echo "β†’ host Ollama reachable at ${host_ip}:11434 β€” routing containers there (local model, not extractive)."
  else
    echo "β†’ host Ollama not detected β€” API will use its configured/auto LLM (extractive fallback if none)."
  fi
fi

# Same reasoning for a host-side vLLM server: inside the container `localhost`
# is the container, so a vLLM on the host is only reachable via the LAN IP.
existing_vllm="${AURALYNQ_LLM__VLLM_BASE_URL:-$(grep -E '^AURALYNQ_LLM__VLLM_BASE_URL=' .env 2>/dev/null | cut -d= -f2- || true)}"
if [ -z "${existing_vllm}" ]; then
  vllm_port="${AURALYNQ_VLLM_PORT:-8001}"
  vllm_host_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1 || true)"
  if [ -n "${vllm_host_ip}" ]; then
    export AURALYNQ_LLM__VLLM_BASE_URL="http://${vllm_host_ip}:${vllm_port}/v1"
    if curl -fsS -m 2 "http://${vllm_host_ip}:${vllm_port}/v1/models" >/dev/null 2>&1; then
      echo "β†’ host vLLM reachable at ${vllm_host_ip}:${vllm_port} β€” routing containers there."
    fi
  fi
fi

# ── Optional NVIDIA GPU visibility for hardware detection ─────────────────────
# The API's ModelFit page reports the host's real GPUs. In a rootless container
# without the nvidia-container-toolkit those GPUs are invisible, so the report
# would wrongly say "CPU only". nvidia-smi only needs its binary + libnvidia-ml
# + the /dev/nvidia* device nodes to enumerate GPUs (no CUDA runtime, no toolkit).
# When they exist on the host we generate a gitignored compose override that
# read-only-mounts them into the api container. This is detection-only; inference
# still runs on host Ollama. compose.gpu.yml is regenerated each start.
GPU_OVERRIDE="compose.gpu.yml"
gpu_args=()
rm -f "$GPU_OVERRIDE"
smi_path="$(command -v nvidia-smi 2>/dev/null || true)"
if [ -n "$smi_path" ] && [ -e /dev/nvidiactl ]; then
  libml="$(ldconfig -p 2>/dev/null | grep -m1 'libnvidia-ml.so.1' | awk '{print $NF}')"
  libml="${libml:-/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1}"
  if [ -e "$libml" ]; then
    {
      echo "# Auto-generated by scripts/stack_up.sh β€” host NVIDIA GPU visibility for"
      echo "# ModelFit hardware detection (nvidia-smi only). Do not edit; regenerated."
      echo "services:"
      echo "  api:"
      echo "    devices:"
      for d in /dev/nvidiactl /dev/nvidia-uvm /dev/nvidia-uvm-tools /dev/nvidia0 /dev/nvidia1 /dev/nvidia2 /dev/nvidia3 /dev/nvidia4 /dev/nvidia5 /dev/nvidia6 /dev/nvidia7; do
        [ -e "$d" ] && echo "      - \"$d\""
      done
      echo "    volumes:"
      echo "      - \"$smi_path:/usr/bin/nvidia-smi:ro\""
      echo "      - \"$libml:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:ro\""
    } > "$GPU_OVERRIDE"
    gpu_args=(-f "$GPU_OVERRIDE")
    echo "β†’ NVIDIA GPU(s) detected β€” exposing nvidia-smi to the API for accurate hardware detection."
  fi
fi

echo "β†’ starting stack…"
$COMPOSE -f "$CF" "${gpu_args[@]}" up -d

# The compose `up` above creates the project network at cniVersion 1.0.0, which
# the older `firewall` CNI plugin rejects (noisy validation warnings on every
# later podman command). Patch it down to 0.4.0 + dnsname now that it exists so
# subsequent `podman ps/logs/exec` and the next `up` run clean.
shopt -s nullglob
for f in "${CNI_DIR}"/*_default.conflist "${CNI_DIR}"/auralynq*.conflist; do
  patch_conflist "$f"
done
shopt -u nullglob

echo "βœ“ stack up (services resolve peers by container_name via dnsname):"
echo "    HTTPS   : https://<SERVER_IP>:${https_port}   (public β€” Caddy TLS proxy)"
echo "    web/api : internal (${bind_internal} loopback), fronted by Caddy"
echo "    Qdrant  : internal (${bind_internal} loopback)"
echo "    Phoenix : internal (${bind_internal} loopback)"