Initial import: bare-metal stacks, Server UI, GPU fan, tutorials.
Infrastructure configs for GMKtec K11 (Docker, vLLM, LocalAI, ComfyUI, control-plane, gpu-fan agent, Server UI with CLI/file explorer/GPU fan curve). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure gpu-fan agent for localhost + ensure API key. UI is in Server UI :8091.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
|
||||
CONTROL_PLANE_ENV="/opt/control-plane/.env"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Run as root: sudo ${SCRIPT_DIR}/enable-lan.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${SCRIPT_DIR}/install.sh"
|
||||
|
||||
set_env_var() {
|
||||
local file="$1" key="$2" val="$3"
|
||||
if grep -q "^${key}=" "${file}"; then
|
||||
sed -i "s|^${key}=.*|${key}=${val}|" "${file}"
|
||||
else
|
||||
echo "${key}=${val}" >> "${file}"
|
||||
fi
|
||||
}
|
||||
|
||||
for pair in "GPU_FAN_API_HOST=127.0.0.1" "GPU_FAN_API_PORT=18090"; do
|
||||
set_env_var "${CONTROL_PLANE_ENV}" "${pair%%=*}" "${pair#*=}"
|
||||
done
|
||||
|
||||
if ! grep -q '^API_KEY=.\{8,\}' "${CONTROL_PLANE_ENV}"; then
|
||||
KEY="$(openssl rand -hex 16)"
|
||||
set_env_var "${CONTROL_PLANE_ENV}" "API_KEY" "${KEY}"
|
||||
echo "Generated new API_KEY in ${CONTROL_PLANE_ENV}"
|
||||
else
|
||||
echo "API_KEY already set in ${CONTROL_PLANE_ENV}"
|
||||
fi
|
||||
|
||||
systemctl restart gpu-fan
|
||||
sleep 2
|
||||
|
||||
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
|
||||
echo ""
|
||||
echo "gpu-fan agent: $(systemctl is-active gpu-fan)"
|
||||
echo "Agent API: http://127.0.0.1:18090 (localhost only)"
|
||||
echo ""
|
||||
echo "GPU Fan panel (LAN):"
|
||||
echo " http://${LAN_IP:-<server-ip>}:8091 → zakładka GPU Fan"
|
||||
echo ""
|
||||
echo "API key (Server UI + gpu-fan):"
|
||||
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
|
||||
INSTALL_DIR="/opt/gpu-fan"
|
||||
CONFIG_DIR="/etc/gpu-fan"
|
||||
SERVICE_NAME="gpu-fan.service"
|
||||
AGENT_PORT=18090
|
||||
CONTROL_PLANE_ENV="/opt/control-plane/.env"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Run as root: sudo ${SCRIPT_DIR}/install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== GPU Fan Control — install ==="
|
||||
|
||||
bash "${SERVER_UI_DIR}/scripts/setup-control-plane-env.sh"
|
||||
|
||||
apt-get update -qq
|
||||
apt-get install -y python3-venv python3-pip
|
||||
|
||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}"
|
||||
|
||||
rsync -a --delete \
|
||||
--exclude '.venv' \
|
||||
--exclude '.env' \
|
||||
--exclude '__pycache__' \
|
||||
"${STACK_DIR}/" "${INSTALL_DIR}/"
|
||||
|
||||
if [[ ! -f "${CONFIG_DIR}/curve.json" ]]; then
|
||||
cp "${INSTALL_DIR}/curve.default.json" "${CONFIG_DIR}/curve.json"
|
||||
echo "Installed default curve: ${CONFIG_DIR}/curve.json"
|
||||
fi
|
||||
|
||||
python3 -m venv "${INSTALL_DIR}/.venv"
|
||||
"${INSTALL_DIR}/.venv/bin/pip" install --upgrade pip -q
|
||||
"${INSTALL_DIR}/.venv/bin/pip" install -r "${INSTALL_DIR}/requirements.txt" -q
|
||||
|
||||
install -m 644 "${INSTALL_DIR}/gpu-fan.service" "/etc/systemd/system/${SERVICE_NAME}"
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${SERVICE_NAME}"
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
sleep 2
|
||||
|
||||
API_KEY_VAL="$(grep '^API_KEY=' "${CONTROL_PLANE_ENV}" | cut -d= -f2- || true)"
|
||||
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
|
||||
echo ""
|
||||
echo "Installed to ${INSTALL_DIR}"
|
||||
echo "Service: $(systemctl is-active "${SERVICE_NAME}" 2>/dev/null || echo unknown)"
|
||||
echo "Env: ${CONTROL_PLANE_ENV}"
|
||||
echo "Curve config: ${CONFIG_DIR}/curve.json"
|
||||
echo "Agent API: 127.0.0.1:${AGENT_PORT} (localhost only)"
|
||||
echo ""
|
||||
echo "GPU Fan UI (Server UI, LAN):"
|
||||
echo " http://${LAN_IP:-<server-ip>}:8091/#gpu-fan"
|
||||
echo ""
|
||||
echo "API key:"
|
||||
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
|
||||
echo ""
|
||||
|
||||
if ss -tlnp 2>/dev/null | grep -q ":${AGENT_PORT}"; then
|
||||
echo "Port ${AGENT_PORT}: listening"
|
||||
if [[ -n "${API_KEY_VAL}" ]]; then
|
||||
STATUS="$(curl -sf "http://127.0.0.1:${AGENT_PORT}/api/status" -H "X-API-Key: ${API_KEY_VAL}" 2>/dev/null | head -c 120 || true)"
|
||||
echo "Agent status: ${STATUS}..."
|
||||
fi
|
||||
else
|
||||
echo "WARN: port ${AGENT_PORT} not listening — check: journalctl -u ${SERVICE_NAME} -n 20"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Logs:"
|
||||
echo " journalctl -u ${SERVICE_NAME} -f"
|
||||
echo ""
|
||||
echo "Pełna instalacja (gpu-fan + Server UI):"
|
||||
echo " sudo ${SERVER_UI_DIR}/scripts/install-control-plane.sh"
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Self-test: curve logic, NVML read, API (dry-run). Full fan write needs sudo.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
mkdir -p config
|
||||
cp -n curve.default.json config/curve.json 2>/dev/null || true
|
||||
|
||||
export CURVE_PATH="${STACK_DIR}/config/curve.json"
|
||||
export DRY_RUN=true
|
||||
export GPU_FAN_PORT=18090
|
||||
|
||||
echo "=== 1. Curve logic ==="
|
||||
.venv/bin/python -c "
|
||||
from fan_controller import parse_curve, interpolate_speed
|
||||
pts = parse_curve({'30':50,'40':65,'50':80,'55':90,'60':100,'70':100})
|
||||
assert interpolate_speed(45, pts) in (72, 73)
|
||||
assert interpolate_speed(70, pts) == 100
|
||||
print('OK')
|
||||
"
|
||||
|
||||
echo "=== 2. NVML read ==="
|
||||
.venv/bin/python -c "
|
||||
import pynvml
|
||||
pynvml.nvmlInit()
|
||||
h = pynvml.nvmlDeviceGetHandleByIndex(0)
|
||||
t = pynvml.nvmlDeviceGetTemperature(h, pynvml.NVML_TEMPERATURE_GPU)
|
||||
print(f'GPU temp: {t}C')
|
||||
pynvml.nvmlShutdown()
|
||||
print('OK')
|
||||
"
|
||||
|
||||
echo "=== 3. API dry-run (background) ==="
|
||||
.venv/bin/python app.py &
|
||||
APP_PID=$!
|
||||
trap 'kill $APP_PID 2>/dev/null || true' EXIT
|
||||
sleep 2
|
||||
|
||||
STATUS=$(curl -sf "http://127.0.0.1:${GPU_FAN_PORT}/api/status")
|
||||
echo "$STATUS" | .venv/bin/python -m json.tool | head -20
|
||||
|
||||
TARGET=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['target_speed_pct'])")
|
||||
TEMP=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['temperature_c'])")
|
||||
echo "Temp=${TEMP}C target=${TARGET}%"
|
||||
|
||||
curl -sf -X PUT "http://127.0.0.1:${GPU_FAN_PORT}/api/curve" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"points":[{"temp":30,"speed":50},{"temp":40,"speed":65},{"temp":50,"speed":80},{"temp":55,"speed":90},{"temp":60,"speed":100},{"temp":70,"speed":100}]}' \
|
||||
| .venv/bin/python -m json.tool >/dev/null
|
||||
echo "Curve PUT: OK"
|
||||
|
||||
curl -sf -X POST "http://127.0.0.1:${GPU_FAN_PORT}/api/mode" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"manual","speed":100}' >/dev/null
|
||||
echo "Mode manual 100%: OK"
|
||||
|
||||
kill $APP_PID 2>/dev/null || true
|
||||
trap - EXIT
|
||||
|
||||
echo ""
|
||||
if sudo -n true 2>/dev/null; then
|
||||
echo "=== 4. Fan write (root) ==="
|
||||
sudo -n env CURVE_PATH="$CURVE_PATH" .venv/bin/python -c "
|
||||
from pathlib import Path
|
||||
from fan_controller import FanController
|
||||
c = FanController(Path('$CURVE_PATH'), 0, 2.0)
|
||||
c.init_nvml()
|
||||
c.set_mode('manual', 50)
|
||||
m = c.update_once()
|
||||
print(f\"Applied: {m['temperature_c']}C -> {m['target_speed_pct']}%\")
|
||||
c.set_mode('auto')
|
||||
c.update_once()
|
||||
c.shutdown()
|
||||
print('OK')
|
||||
"
|
||||
else
|
||||
echo "=== 4. Fan write (root) — SKIP (sudo needs password) ==="
|
||||
echo " Run: sudo scripts/install.sh && sudo systemctl start gpu-fan"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "All automated checks passed."
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
CONTROL_PLANE_ENV="${STACK_DIR}/../control-plane/.env"
|
||||
EXAMPLE="${STACK_DIR}/../control-plane/.env.example"
|
||||
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
if [[ ! -f "${CONTROL_PLANE_ENV}" ]]; then
|
||||
cp "${EXAMPLE}" "${CONTROL_PLANE_ENV}"
|
||||
echo "Created ${CONTROL_PLANE_ENV} from example"
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${CONTROL_PLANE_ENV}"
|
||||
set +a
|
||||
|
||||
export CONTROL_PLANE_ENV="${CONTROL_PLANE_ENV}"
|
||||
|
||||
CURVE_PATH="${CURVE_PATH:-/etc/gpu-fan/curve.json}"
|
||||
export CURVE_PATH
|
||||
|
||||
if [[ ! -f "${CURVE_PATH}" ]]; then
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
mkdir -p "$(dirname "${CURVE_PATH}")"
|
||||
cp curve.default.json "${CURVE_PATH}"
|
||||
else
|
||||
mkdir -p "${STACK_DIR}/config"
|
||||
CURVE_PATH="${STACK_DIR}/config/curve.json"
|
||||
export CURVE_PATH
|
||||
[[ -f "${CURVE_PATH}" ]] || cp curve.default.json "${CURVE_PATH}"
|
||||
echo "Using local curve: ${CURVE_PATH}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -d .venv ]]; then
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -q -r requirements.txt
|
||||
fi
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "ERROR: Fan control requires root. Use:"
|
||||
echo " sudo -E ${SCRIPT_DIR}/start.sh"
|
||||
echo "Or install system-wide:"
|
||||
echo " sudo ${SCRIPT_DIR}/install.sh && sudo systemctl start gpu-fan"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet gpu-fan 2>/dev/null; then
|
||||
echo "ERROR: gpu-fan.service is already running (systemd)."
|
||||
echo " Use: sudo systemctl stop gpu-fan"
|
||||
echo " Or: sudo journalctl -u gpu-fan -f"
|
||||
echo ""
|
||||
echo "Do not run scripts/start.sh alongside systemd — one instance only."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT="${GPU_FAN_API_PORT:-18090}"
|
||||
HOST="${GPU_FAN_API_HOST:-127.0.0.1}"
|
||||
|
||||
if ss -tln "sport = :${PORT}" 2>/dev/null | grep -q LISTEN; then
|
||||
echo "ERROR: Port ${PORT} is already in use."
|
||||
echo ""
|
||||
ss -tlnp "sport = :${PORT}" 2>/dev/null || ss -tln "sport = :${PORT}"
|
||||
echo ""
|
||||
echo "Common causes:"
|
||||
echo " - Suspended foreground start (Ctrl+Z) — run: jobs -l && kill %<n>"
|
||||
echo " - Orphan process — run: sudo ${SCRIPT_DIR}/status.sh"
|
||||
echo " - systemd still stopping — wait a few seconds"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== GPU Fan Control (foreground) ==="
|
||||
echo "Agent API: http://${HOST}:${PORT}"
|
||||
echo "Curve: ${CURVE_PATH}"
|
||||
echo ""
|
||||
echo "Stop with Ctrl+C (not Ctrl+Z — suspended process keeps the port)."
|
||||
echo ""
|
||||
|
||||
exec .venv/bin/python app.py
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnose gpu-fan port conflicts and running instances.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
OPT_ENV="/opt/control-plane/.env"
|
||||
REPO_ENV="${STACK_DIR}/../control-plane/.env"
|
||||
|
||||
echo "=== gpu-fan status ==="
|
||||
echo ""
|
||||
|
||||
if systemctl list-unit-files gpu-fan.service &>/dev/null; then
|
||||
echo "systemd:"
|
||||
systemctl is-active gpu-fan 2>/dev/null && systemctl status gpu-fan --no-pager -l 2>/dev/null | head -8 || echo " inactive"
|
||||
else
|
||||
echo "systemd: gpu-fan.service not installed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "Ports 8090–8099:"
|
||||
if ss -tlnp 2>/dev/null | grep -E ':809[0-9]' ; then
|
||||
:
|
||||
elif ss -tln 2>/dev/null | grep -E ':809[0-9]' ; then
|
||||
echo " (run as root for process names: sudo ss -tlnp)"
|
||||
else
|
||||
echo " (none listening)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "gpu-fan processes:"
|
||||
pgrep -af '/opt/gpu-fan/.venv/bin/python|stacks/gpu-fan/.venv/bin/python|gpu-fan/app.py' 2>/dev/null \
|
||||
|| echo " (none)"
|
||||
echo ""
|
||||
|
||||
echo "Shell suspended jobs (Ctrl+Z):"
|
||||
if jobs -l 2>/dev/null | grep -q .; then
|
||||
jobs -l
|
||||
echo " Kill with: kill %<job-number>"
|
||||
else
|
||||
echo " (none in this shell)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "GPU_FAN_API_PORT config:"
|
||||
[[ -f "${OPT_ENV}" ]] && echo " /opt/control-plane/.env: $(grep '^GPU_FAN_API_PORT=' "${OPT_ENV}" || echo '(not set)')"
|
||||
[[ -f "${REPO_ENV}" ]] && echo " repo control-plane: $(grep '^GPU_FAN_API_PORT=' "${REPO_ENV}" || echo '(not set)')"
|
||||
echo " Note: production uses /opt/control-plane/.env (shared with Server UI)"
|
||||
echo ""
|
||||
|
||||
if [[ "${1:-}" == "--cleanup" ]]; then
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "ERROR: --cleanup requires root: sudo ${SCRIPT_DIR}/status.sh --cleanup"
|
||||
exit 1
|
||||
fi
|
||||
echo "=== cleanup ==="
|
||||
systemctl stop gpu-fan 2>/dev/null || true
|
||||
pkill -f '/opt/gpu-fan/.venv/bin/python /opt/gpu-fan/app.py' 2>/dev/null || true
|
||||
pkill -f 'stacks/gpu-fan/.venv/bin/python app.py' 2>/dev/null || true
|
||||
sleep 0.5
|
||||
if ss -tln 2>/dev/null | grep -qE ':809[0-9]'; then
|
||||
echo "WARNING: ports still in use:"
|
||||
ss -tlnp 2>/dev/null | grep -E ':809[0-9]' || ss -tln | grep -E ':809[0-9]'
|
||||
echo "Check suspended jobs in other shells: jobs -l"
|
||||
else
|
||||
echo "Ports 809x are free."
|
||||
fi
|
||||
echo ""
|
||||
echo "Start production instance:"
|
||||
echo " sudo systemctl start gpu-fan"
|
||||
echo "Or foreground debug (systemd stopped):"
|
||||
echo " sudo ${SCRIPT_DIR}/start.sh"
|
||||
fi
|
||||
Reference in New Issue
Block a user