Compare commits
13
Commits
f6ac45d73f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c5b4865a | ||
|
|
2b3e0e605a | ||
|
|
4a2a00966d | ||
|
|
162e03dad2 | ||
|
|
cc06215319 | ||
|
|
c9a2e73c61 | ||
|
|
9bd3d1052d | ||
|
|
b77d275fe0 | ||
|
|
a7a686499e | ||
|
|
afd786394a | ||
|
|
19fac12ff6 | ||
|
|
f99baf0d92 | ||
|
|
bdf42e74a3 |
@@ -0,0 +1,2 @@
|
||||
# Backups locales: pueden contener claves y secretos
|
||||
ubuntu-config-*.tar.gz
|
||||
@@ -0,0 +1,525 @@
|
||||
# OpenClaw con servidor local
|
||||
|
||||
Guía para ejecutar OpenClaw usando el modelo local administrado por Ollama y
|
||||
expuesto mediante LiteLLM.
|
||||
|
||||
> El video de referencia es [Aprende OpenClaw Ahora! curso completo desde cero
|
||||
> para programadores](https://www.youtube.com/watch?v=4UtyJt2rMfo). Esta guía
|
||||
> adapta el flujo de configuración a este servidor local y no depende de un
|
||||
> proveedor cloud.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```text
|
||||
OpenClaw Gateway
|
||||
|
|
||||
| OpenAI-compatible: http://127.0.0.1:8000/v1
|
||||
v
|
||||
LiteLLM Proxy
|
||||
|
|
||||
| Ollama native API: http://127.0.0.1:11434
|
||||
v
|
||||
Ollama -> qwen2.5-coder:14b -> NVIDIA GPU
|
||||
```
|
||||
|
||||
Cuando OpenClaw y el servidor LLM están en la misma máquina, usa `127.0.0.1`.
|
||||
Si OpenClaw corre en otro equipo, reemplaza esa dirección por la IP privada del
|
||||
servidor, por ejemplo `http://192.168.1.50:8000/v1`.
|
||||
|
||||
## Requisitos
|
||||
|
||||
En el servidor LLM:
|
||||
|
||||
- Ubuntu 22.04 o 24.04.
|
||||
- Node.js 24.16 o superior para OpenClaw.
|
||||
- GPU NVIDIA con controladores funcionales.
|
||||
- 16 GB de RAM como mínimo y espacio para el modelo.
|
||||
- `sudo`, `curl`, `git`, Python 3 y conexión a Internet.
|
||||
|
||||
En el cliente remoto solo necesitas Node.js compatible con OpenClaw y acceso de
|
||||
red al puerto `8000` del servidor.
|
||||
|
||||
## 1. Preparar el servidor LLM
|
||||
|
||||
Clona este repositorio y ejecuta el instalador principal:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
|
||||
cd llm-server-setup
|
||||
chmod +x setup_llm_server.sh
|
||||
sudo ./setup_llm_server.sh
|
||||
```
|
||||
|
||||
El script instala y configura:
|
||||
|
||||
- controladores NVIDIA y un límite de potencia de 300 W;
|
||||
- 8 GB de swap;
|
||||
- Ollama como servicio systemd en `0.0.0.0:11434`;
|
||||
- el modelo `qwen2.5-coder:14b`;
|
||||
- LiteLLM en `/opt/litellm-env`;
|
||||
- LiteLLM como `litellm.service` en `0.0.0.0:8000`;
|
||||
- reglas UFW para SSH y LiteLLM.
|
||||
|
||||
Si se instalaron nuevos controladores, reinicia antes de continuar:
|
||||
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
## 2. Verificar Ollama y LiteLLM
|
||||
|
||||
Ejecuta estas comprobaciones en el servidor:
|
||||
|
||||
```bash
|
||||
systemctl is-active ollama
|
||||
systemctl is-active litellm
|
||||
nvidia-smi
|
||||
curl http://127.0.0.1:11434/api/tags
|
||||
curl http://127.0.0.1:8000/health/liveliness
|
||||
```
|
||||
|
||||
Confirma que el modelo existe:
|
||||
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
La configuración generada por `setup_llm_server.sh` está en:
|
||||
|
||||
```text
|
||||
~/litellm_config.yaml
|
||||
```
|
||||
|
||||
El servicio LiteLLM debe ejecutarse con esa configuración y escuchar en el
|
||||
puerto `8000`. Para revisar errores:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u ollama -n 100 --no-pager
|
||||
sudo journalctl -u litellm -n 100 --no-pager
|
||||
```
|
||||
|
||||
## 3. Probar el endpoint OpenAI-compatible
|
||||
|
||||
Antes de instalar OpenClaw, prueba LiteLLM directamente:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/v1/models
|
||||
```
|
||||
|
||||
Después envía una consulta usando una clave local de prueba. El proxy de este
|
||||
repositorio no configura autenticación, pero OpenClaw espera un valor de API
|
||||
key para el proveedor LiteLLM; `local` sirve como valor de configuración local:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer local' \
|
||||
-d '{
|
||||
"model": "qwen2.5-coder:14b",
|
||||
"messages": [{"role": "user", "content": "Responde exactamente: OK"}],
|
||||
"temperature": 0
|
||||
}'
|
||||
```
|
||||
|
||||
La respuesta debe contener `OK` y no un error de conexión o de modelo.
|
||||
|
||||
## 4. Instalar OpenClaw
|
||||
|
||||
En el servidor o en el equipo donde se ejecutará el Gateway:
|
||||
|
||||
```bash
|
||||
bash install_openclaw.sh
|
||||
```
|
||||
|
||||
El script instala OpenClaw sin iniciar el asistente. También puedes ejecutar el
|
||||
instalador oficial directamente:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
|
||||
```
|
||||
|
||||
Comprueba Node.js y OpenClaw:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
## 5. Configurar OpenClaw con LiteLLM local
|
||||
|
||||
La configuración de OpenClaw se guarda en:
|
||||
|
||||
```text
|
||||
~/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Crea o edita ese archivo como JSON5:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
apiKey: "local",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "qwen2.5-coder:14b",
|
||||
name: "Qwen 2.5 Coder 14B local",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 32768
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "litellm/qwen2.5-coder:14b"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Puntos importantes:
|
||||
|
||||
- En `baseUrl` incluye `/v1` porque OpenClaw usa la API compatible con OpenAI de
|
||||
LiteLLM.
|
||||
- No uses `http://127.0.0.1:11434/v1` directamente con el proveedor LiteLLM.
|
||||
- Para Ollama nativo, OpenClaw usa otra configuración y no debe mezclarse con
|
||||
esta ruta.
|
||||
- `apiKey: "local"` coincide con el valor usado en las pruebas. No es una
|
||||
credencial real porque el servicio actual no habilita autenticación.
|
||||
|
||||
Valida la configuración:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw models list --provider litellm --refresh --json
|
||||
```
|
||||
|
||||
## 6. Configurar con el asistente
|
||||
|
||||
Como alternativa a editar JSON5, ejecuta el onboarding indicando LiteLLM:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice litellm-api-key
|
||||
```
|
||||
|
||||
Cuando solicite la URL del proxy, usa:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/v1
|
||||
```
|
||||
|
||||
Y como API key local:
|
||||
|
||||
```text
|
||||
local
|
||||
```
|
||||
|
||||
Si el asistente permite modo no interactivo en tu versión instalada:
|
||||
|
||||
```bash
|
||||
export LITELLM_API_KEY=local
|
||||
openclaw onboard --non-interactive --accept-risk --skip-health \
|
||||
--auth-choice litellm-api-key \
|
||||
--litellm-api-key "$LITELLM_API_KEY" \
|
||||
--custom-base-url "http://127.0.0.1:8000/v1"
|
||||
```
|
||||
|
||||
Después revisa el modelo primario con:
|
||||
|
||||
```bash
|
||||
openclaw config get agents.defaults.model.primary
|
||||
```
|
||||
|
||||
Debe devolver `litellm/qwen2.5-coder:14b`.
|
||||
|
||||
## 7. Instalar y verificar el Gateway
|
||||
|
||||
Instala el servicio de usuario de OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw gateway install
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
El Gateway normalmente escucha en el puerto `18789`. Abre el dashboard:
|
||||
|
||||
```bash
|
||||
openclaw dashboard
|
||||
```
|
||||
|
||||
También puedes verificarlo desde el navegador en:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:18789
|
||||
```
|
||||
|
||||
Prueba un mensaje desde el dashboard. En otra terminal puedes observar los
|
||||
logs del Gateway:
|
||||
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
## 8. Acceder desde Windows con PuTTY o PowerShell
|
||||
|
||||
En este servidor, la IP de la máquina Ubuntu es:
|
||||
|
||||
```text
|
||||
192.168.0.225
|
||||
```
|
||||
|
||||
El Gateway está enlazado intencionalmente a `127.0.0.1:18789`, por lo que no
|
||||
debes abrir `192.168.0.225:18789` directamente. Usa un túnel SSH desde el PC
|
||||
Windows.
|
||||
|
||||
### PowerShell
|
||||
|
||||
Ejecuta este comando en PowerShell del PC cliente:
|
||||
|
||||
```powershell
|
||||
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
|
||||
```
|
||||
|
||||
Introduce la contraseña de `ctello` y deja esa ventana abierta. El parámetro
|
||||
`-L` significa:
|
||||
|
||||
```text
|
||||
Puerto local del PC:18789 -> servidor:127.0.0.1:18789
|
||||
```
|
||||
|
||||
En otra ventana de PowerShell, comprueba el túnel:
|
||||
|
||||
```powershell
|
||||
Test-NetConnection 127.0.0.1 -Port 18789
|
||||
```
|
||||
|
||||
Debe mostrar:
|
||||
|
||||
```text
|
||||
TcpTestSucceeded : True
|
||||
```
|
||||
|
||||
Después abre el dashboard en el navegador del PC:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:18789
|
||||
```
|
||||
|
||||
Si el puerto local `18789` ya está ocupado, usa otro puerto solo en el PC:
|
||||
|
||||
```powershell
|
||||
ssh -N -L 18790:127.0.0.1:18789 ctello@192.168.0.225
|
||||
```
|
||||
|
||||
En ese caso abre:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:18790
|
||||
```
|
||||
|
||||
El destino remoto sigue siendo siempre `127.0.0.1:18789`.
|
||||
|
||||
### PuTTY
|
||||
|
||||
Configura la sesión con:
|
||||
|
||||
```text
|
||||
Host Name: 192.168.0.225
|
||||
Port: 22
|
||||
Connection type: SSH
|
||||
```
|
||||
|
||||
Después ve a `Connection > SSH > Tunnels` y añade:
|
||||
|
||||
```text
|
||||
Source port: 18789
|
||||
Destination: 127.0.0.1:18789
|
||||
Type: Local
|
||||
```
|
||||
|
||||
Pulsa `Add`, conecta la sesión y abre en el navegador:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:18789
|
||||
```
|
||||
|
||||
Mantén abierta la sesión SSH mientras uses el dashboard.
|
||||
|
||||
## 9. Usar OpenClaw desde otro equipo
|
||||
|
||||
Si el Gateway corre en otro equipo distinto al servidor LLM, cambia únicamente
|
||||
`baseUrl` en `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "http://192.168.1.50:8000/v1",
|
||||
apiKey: "local",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "qwen2.5-coder:14b",
|
||||
name: "Qwen 2.5 Coder 14B local",
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 32768
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "litellm/qwen2.5-coder:14b" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
En el servidor, permite el acceso solo desde la red privada. Por ejemplo,
|
||||
reemplaza la regla abierta actual de UFW por una regla limitada a tu subred:
|
||||
|
||||
```bash
|
||||
sudo ufw delete allow 8000/tcp
|
||||
sudo ufw allow from 192.168.1.0/24 to any port 8000 proto tcp
|
||||
sudo ufw status verbose
|
||||
```
|
||||
|
||||
No expongas el puerto `8000` directamente a Internet: el proxy actual no tiene
|
||||
una API key real ni TLS.
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Gateway activo, pero el navegador muestra `ERR_CONNECTION_REFUSED`
|
||||
|
||||
Comprueba el estado en Ubuntu:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
ss -ltnp | grep 18789
|
||||
```
|
||||
|
||||
Debe aparecer:
|
||||
|
||||
```text
|
||||
Runtime: running
|
||||
Connectivity probe: ok
|
||||
Listening: 127.0.0.1:18789
|
||||
```
|
||||
|
||||
Si el Gateway no está activo, configura el modo local y arráncalo:
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.mode local
|
||||
openclaw gateway install
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
Si el servicio de usuario necesita una sesión persistente, habilita el
|
||||
`linger` para `ctello` desde una cuenta con `sudo`:
|
||||
|
||||
```bash
|
||||
sudo loginctl enable-linger ctello
|
||||
```
|
||||
|
||||
Después vuelve a iniciar sesión SSH como `ctello` y ejecuta:
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now openclaw-gateway.service
|
||||
```
|
||||
|
||||
Si `systemctl --user` muestra que faltan `DBUS_SESSION_BUS_ADDRESS` o
|
||||
`XDG_RUNTIME_DIR`, vuelve a conectarte por SSH después de habilitar `linger`.
|
||||
No ejecutes un segundo `openclaw gateway run` si el servicio ya está activo.
|
||||
|
||||
Para revisar errores:
|
||||
|
||||
```bash
|
||||
journalctl --user -u openclaw-gateway.service -n 100 --no-pager
|
||||
```
|
||||
|
||||
### El túnel de PowerShell no conecta
|
||||
|
||||
En el PC cliente verifica primero que SSH funciona:
|
||||
|
||||
```powershell
|
||||
Test-NetConnection 192.168.0.225 -Port 22
|
||||
```
|
||||
|
||||
Si SSH responde pero el puerto del túnel no, comprueba en Ubuntu que el
|
||||
Gateway esté escuchando en `127.0.0.1:18789`. No abras el puerto `18789` en
|
||||
UFW ni cambies el Gateway a `0.0.0.0` solo para evitar el túnel.
|
||||
|
||||
### `Connection refused` en el puerto 8000
|
||||
|
||||
```bash
|
||||
sudo systemctl restart ollama litellm
|
||||
sudo systemctl status ollama litellm
|
||||
sudo journalctl -u litellm -n 100 --no-pager
|
||||
```
|
||||
|
||||
### LiteLLM no encuentra el modelo
|
||||
|
||||
```bash
|
||||
ollama list
|
||||
ollama pull qwen2.5-coder:14b
|
||||
curl http://127.0.0.1:8000/v1/models
|
||||
```
|
||||
|
||||
El nombre debe coincidir exactamente en los tres lugares:
|
||||
|
||||
```text
|
||||
qwen2.5-coder:14b
|
||||
```
|
||||
|
||||
### OpenClaw muestra respuestas de herramientas como texto
|
||||
|
||||
Comprueba que OpenClaw usa LiteLLM con `/v1` y no la URL equivocada de Ollama:
|
||||
|
||||
```bash
|
||||
openclaw config get models.providers.litellm.baseUrl
|
||||
```
|
||||
|
||||
Debe devolver:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/v1
|
||||
```
|
||||
|
||||
### El modelo se queda sin memoria
|
||||
|
||||
Reduce `contextWindow` y `maxTokens` en la configuración de OpenClaw y revisa
|
||||
la VRAM disponible:
|
||||
|
||||
```bash
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
### La configuración no es válida
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw doctor --fix
|
||||
```
|
||||
|
||||
OpenClaw valida estrictamente `openclaw.json`; un campo desconocido o un tipo
|
||||
incorrecto puede impedir que el Gateway arranque.
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Documentación oficial de OpenClaw](https://docs.openclaw.ai/)
|
||||
- [Proveedor LiteLLM en OpenClaw](https://docs.openclaw.ai/providers/litellm)
|
||||
- [Proveedor Ollama en OpenClaw](https://docs.openclaw.ai/providers/ollama)
|
||||
- [Configuración del Gateway](https://docs.openclaw.ai/gateway/configuration)
|
||||
@@ -1,5 +1,231 @@
|
||||
nano setup_llm_server.sh
|
||||
# LLM Server
|
||||
|
||||
Despliegue automatizado de un servidor LLM local con Ollama, LiteLLM y
|
||||
OpenClaw sobre Ubuntu 22.04/24.04.
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Ubuntu 22.04 o 24.04
|
||||
- CPU de 4 núcleos o superior
|
||||
- 16 GB de RAM como mínimo
|
||||
- GPU NVIDIA compatible con sus controladores Linux
|
||||
- Acceso `sudo` y conexión a Internet
|
||||
|
||||
La configuración está orientada a una NVIDIA RTX 3090 de 24 GB, pero el
|
||||
script puede utilizarse con otras GPU compatibles ajustando el modelo si es
|
||||
necesario.
|
||||
|
||||
## Instalación
|
||||
|
||||
Clona el repositorio en el servidor:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
|
||||
cd llm-server-setup
|
||||
```
|
||||
|
||||
Ejecuta el instalador principal como `root` o mediante `sudo`:
|
||||
|
||||
```bash
|
||||
chmod +x setup_llm_server.sh
|
||||
sudo ./setup_llm_server.sh
|
||||
```
|
||||
|
||||
El instalador:
|
||||
|
||||
- actualiza los paquetes del sistema e instala dependencias;
|
||||
- configura un archivo de swap de 8 GB;
|
||||
- instala los controladores NVIDIA si no están disponibles;
|
||||
- instala y configura Ollama en `0.0.0.0:11434`;
|
||||
- crea un entorno virtual de LiteLLM en `/opt/litellm-env`;
|
||||
- descarga `qwen2.5-coder:14b`;
|
||||
- crea el servicio `litellm.service` en el puerto `8000`;
|
||||
- permite SSH y LiteLLM mediante UFW.
|
||||
|
||||
El proceso puede tardar varios minutos y requiere espacio suficiente para el
|
||||
modelo. Si se instalan controladores NVIDIA nuevos, reinicia el servidor:
|
||||
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
## Verificación
|
||||
|
||||
Después de la instalación, comprueba los servicios:
|
||||
|
||||
```bash
|
||||
systemctl status ollama
|
||||
systemctl status litellm
|
||||
nvidia-smi
|
||||
curl http://127.0.0.1:11434/api/tags
|
||||
curl http://127.0.0.1:8000/health/liveliness
|
||||
```
|
||||
|
||||
La configuración de LiteLLM se genera en el directorio personal del usuario
|
||||
que ejecuta `sudo` y queda guardada como `~/litellm_config.yaml`.
|
||||
|
||||
## Instalación de OpenClaw
|
||||
|
||||
Para la configuración completa de OpenClaw con el servidor local, consulta
|
||||
[OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
|
||||
|
||||
El script `install_openclaw.sh` utiliza el instalador oficial de OpenClaw y
|
||||
está pensado para Linux y WSL2. Para instalarlo sin abrir el asistente inicial:
|
||||
|
||||
```bash
|
||||
bash install_openclaw.sh
|
||||
```
|
||||
|
||||
Para instalarlo y ejecutar el asistente inmediatamente:
|
||||
|
||||
```bash
|
||||
bash install_openclaw.sh --onboard
|
||||
```
|
||||
|
||||
Si la instalación se hizo sin asistente, puedes iniciarlo después y configurar
|
||||
el servicio de Gateway:
|
||||
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
Comprueba la instalación con:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
openclaw doctor
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
Si OpenClaw devuelve respuestas vacías o de un token, ejecuta el reparador
|
||||
local como el usuario que ejecuta OpenClaw (`ctello`, no `root`):
|
||||
|
||||
```bash
|
||||
chmod +x fix_openclaw_local.sh
|
||||
bash ./fix_openclaw_local.sh
|
||||
```
|
||||
|
||||
El script respalda `openclaw.json`, fuerza el Gateway local, desactiva el
|
||||
razonamiento predeterminado y la memoria semántica sin embeddings, inicializa
|
||||
el workspace Git y reinicia el Gateway. No borra sesiones ni secretos.
|
||||
|
||||
Después crea una sesión nueva en el dashboard y prueba:
|
||||
|
||||
```text
|
||||
Responde exactamente: SESION NUEVA OK
|
||||
```
|
||||
|
||||
Para acceder al dashboard desde Windows mediante PowerShell, usa un túnel SSH
|
||||
hacia el servidor Ubuntu:
|
||||
|
||||
```powershell
|
||||
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
|
||||
```
|
||||
|
||||
Mantén la sesión abierta y entra desde el navegador en:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:18789
|
||||
```
|
||||
|
||||
La guía completa de acceso desde PuTTY, PowerShell y solución de problemas está
|
||||
en [OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
|
||||
|
||||
## Respaldo y recuperación
|
||||
|
||||
Para ejecutar un diagnóstico independiente y de solo lectura del servidor usa
|
||||
`ubuntu_server_diagnostic.sh`. Comprueba Ubuntu, hardware, servicios, puertos,
|
||||
Ollama, LiteLLM, OpenClaw y UFW:
|
||||
|
||||
```bash
|
||||
chmod +x ubuntu_server_diagnostic.sh
|
||||
sudo ./ubuntu_server_diagnostic.sh --report /tmp/ubuntu-server-test.txt
|
||||
```
|
||||
|
||||
Puedes cambiar los endpoints o el modelo esperado mediante variables de entorno:
|
||||
|
||||
```bash
|
||||
OLLAMA_URL=http://127.0.0.1:11434 \
|
||||
LITELLM_URL=http://127.0.0.1:8000 \
|
||||
MODEL_NAME=qwen2.5-coder:14b \
|
||||
sudo -E ./ubuntu_server_diagnostic.sh
|
||||
```
|
||||
|
||||
Para guardar la configuración del servidor antes de realizar cambios, ejecuta
|
||||
el script como `root`:
|
||||
|
||||
```bash
|
||||
chmod +x backup_ubuntu_config.sh restore_ubuntu_config.sh
|
||||
sudo ./backup_ubuntu_config.sh
|
||||
```
|
||||
|
||||
El respaldo se guarda por defecto en `/var/backups` e incluye inventario de
|
||||
hardware, paquetes, servicios, red, UFW, systemd, NVIDIA, Ollama, LiteLLM,
|
||||
OpenClaw y configuraciones de usuario. No incluye modelos Ollama, el entorno
|
||||
virtual de LiteLLM, bases de datos ni logs completos.
|
||||
|
||||
Las claves SSH, certificados privados y credenciales de OpenClaw quedan fuera
|
||||
por defecto. Para incluirlos explícitamente, protege el archivo resultante:
|
||||
|
||||
```bash
|
||||
sudo ./backup_ubuntu_config.sh --include-secrets
|
||||
sudo chmod 600 /var/backups/ubuntu-config-*.tar.gz
|
||||
```
|
||||
|
||||
En una instalación Ubuntu nueva, instala primero el sistema base y crea los
|
||||
usuarios necesarios. Después copia el archivo de respaldo y usa el restaurador
|
||||
en modo vista previa:
|
||||
|
||||
```bash
|
||||
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz
|
||||
```
|
||||
|
||||
Para aplicar la configuración:
|
||||
|
||||
```bash
|
||||
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz --yes
|
||||
```
|
||||
|
||||
Puedes reinstalar los paquetes APT y arrancar los servicios habilitados de la
|
||||
máquina original de forma explícita:
|
||||
|
||||
```bash
|
||||
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz \
|
||||
--yes --install-packages --start-services
|
||||
```
|
||||
|
||||
Revisa siempre los servicios y el firewall después de restaurar:
|
||||
|
||||
```bash
|
||||
systemctl --failed
|
||||
systemctl status ollama litellm
|
||||
sudo ufw status verbose
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
## Tool calling y rutas de Windows
|
||||
|
||||
Cuando un modelo envía rutas de Windows dentro de argumentos JSON, las barras
|
||||
invertidas deben estar escapadas. Por ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "C:\\Users\\name\\file.txt"
|
||||
}
|
||||
```
|
||||
|
||||
Si las barras no están escapadas, el parser JSON puede fallar antes de que la
|
||||
herramienta llegue a ejecutarse.
|
||||
|
||||
## Archivos principales
|
||||
|
||||
- `setup_llm_server.sh`: instala y configura Ollama, LiteLLM, NVIDIA y UFW.
|
||||
- `install_openclaw.sh`: instala OpenClaw mediante el instalador oficial.
|
||||
- `litellm_config.yaml`: configuración local opcional para LiteLLM.
|
||||
- `tool_call_demo.py`: ejemplo local de manejo de llamadas a herramientas.
|
||||
|
||||
## Seguridad
|
||||
|
||||
El instalador expone Ollama en todas las interfaces y abre el puerto `8000`
|
||||
en UFW. En un servidor conectado a Internet, limita esos puertos a la red
|
||||
privada o protégelos detrás de un proxy con autenticación y HTTPS.
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
readonly DEFAULT_OUTPUT_DIR="/var/backups"
|
||||
|
||||
include_secrets=false
|
||||
output_dir="$DEFAULT_OUTPUT_DIR"
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Uso: sudo $SCRIPT_NAME [opciones]
|
||||
|
||||
Crea un respaldo comprimido de la configuración de Ubuntu para replicarla en
|
||||
otra instalación. No incluye modelos Ollama, entornos virtuales ni bases de datos.
|
||||
|
||||
Opciones:
|
||||
--output-dir DIR Directorio donde guardar el archivo .tar.gz.
|
||||
--include-secrets Incluye claves SSH, certificados privados y secretos
|
||||
de OpenClaw. El archivo resultante debe protegerse.
|
||||
-h, --help Muestra esta ayuda.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output-dir)
|
||||
[[ $# -ge 2 ]] || { echo "Falta DIR para --output-dir" >&2; exit 2; }
|
||||
output_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--include-secrets)
|
||||
include_secrets=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Opción desconocida: $1" >&2
|
||||
show_help >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Ejecuta este script con sudo o como root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
|
||||
command -v gzip >/dev/null || { echo "gzip es obligatorio." >&2; exit 1; }
|
||||
|
||||
hostname_value="$(hostname -s 2>/dev/null || echo unknown)"
|
||||
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup_name="ubuntu-config-${hostname_value}-${timestamp}"
|
||||
work_dir="$(mktemp -d)"
|
||||
archive_path="${output_dir}/${backup_name}.tar.gz"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$work_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$work_dir/metadata" "$work_dir/files" "$work_dir/packages" "$work_dir/services"
|
||||
mkdir -p "$output_dir"
|
||||
umask 077
|
||||
|
||||
copy_path() {
|
||||
local source="$1"
|
||||
local destination="$work_dir/files$source"
|
||||
|
||||
if [[ -e "$source" || -L "$source" ]]; then
|
||||
mkdir -p "$(dirname "$destination")"
|
||||
cp -a "$source" "$destination"
|
||||
fi
|
||||
}
|
||||
|
||||
capture() {
|
||||
local name="$1"
|
||||
shift
|
||||
"$@" > "$work_dir/metadata/$name.txt" 2>&1 || true
|
||||
}
|
||||
|
||||
capture_shell() {
|
||||
local name="$1"
|
||||
local command_text="$2"
|
||||
bash -c "$command_text" > "$work_dir/metadata/$name.txt" 2>&1 || true
|
||||
}
|
||||
|
||||
printf 'Creando respaldo en %s\n' "$archive_path"
|
||||
|
||||
printf '%s\n' "hostname=$hostname_value" "created_at=$(date --iso-8601=seconds)" \
|
||||
"include_secrets=$include_secrets" > "$work_dir/metadata/backup-info.txt"
|
||||
|
||||
capture_shell os-release 'cat /etc/os-release'
|
||||
capture_shell kernel 'uname -a'
|
||||
capture_shell hardware 'lscpu; echo; free -h; echo; lsblk -f'
|
||||
capture_shell disks 'df -hT; echo; findmnt'
|
||||
capture_shell network 'ip -brief address; echo; ip route; echo; resolvectl status 2>/dev/null || true'
|
||||
capture_shell users 'getent passwd | awk -F: '\''$3 >= 1000 || $1 == "root" {print $1 ":" $3 ":" $4 ":" $6}'\'''
|
||||
capture_shell mounts 'mount'
|
||||
# Registrar nombres, no valores que puedan contener credenciales.
|
||||
capture_shell environment 'printenv | cut -d= -f1 | sort -u'
|
||||
capture_shell nvidia 'command -v nvidia-smi && nvidia-smi -q || true'
|
||||
capture_shell ollama 'command -v ollama && ollama list || true'
|
||||
capture_shell versions 'command -v node && node --version || true; command -v npm && npm --version || true; command -v python3 && python3 --version || true; command -v ollama && ollama --version || true; command -v openclaw && openclaw --version || true'
|
||||
capture_shell ufw 'command -v ufw && ufw status verbose || true'
|
||||
capture_shell iptables 'command -v iptables-save && iptables-save || true'
|
||||
capture_shell sysctl 'sysctl -a 2>/dev/null'
|
||||
capture_shell timers 'systemctl list-timers --all --no-pager'
|
||||
capture_shell enabled-services 'systemctl list-unit-files --state=enabled --no-legend --no-pager'
|
||||
capture_shell failed-services 'systemctl --failed --no-pager'
|
||||
|
||||
if command -v dpkg-query >/dev/null; then
|
||||
dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$work_dir/packages/dpkg-status.tsv" || true
|
||||
fi
|
||||
if command -v apt-mark >/dev/null; then
|
||||
apt-mark showmanual | sort > "$work_dir/packages/apt-manual.txt" || true
|
||||
fi
|
||||
if command -v snap >/dev/null; then
|
||||
snap list > "$work_dir/packages/snap-list.txt" 2>&1 || true
|
||||
fi
|
||||
if command -v pip3 >/dev/null; then
|
||||
pip3 freeze > "$work_dir/packages/pip3-freeze.txt" 2>&1 || true
|
||||
fi
|
||||
|
||||
systemctl list-unit-files --type=service --no-legend --no-pager \
|
||||
> "$work_dir/services/all-service-units.txt" 2>&1 || true
|
||||
systemctl list-unit-files --state=enabled --type=service --no-legend --no-pager \
|
||||
| awk '{print $1}' | sort -u > "$work_dir/services/enabled-service-names.txt" || true
|
||||
|
||||
# Configuración del sistema y de los servicios usados por este proyecto.
|
||||
for path in \
|
||||
/etc/apt \
|
||||
/etc/default \
|
||||
/etc/environment \
|
||||
/etc/fstab \
|
||||
/etc/hostname \
|
||||
/etc/hosts \
|
||||
/etc/issue \
|
||||
/etc/netplan \
|
||||
/etc/NetworkManager \
|
||||
/etc/systemd/system \
|
||||
/etc/sysctl.d \
|
||||
/etc/modprobe.d \
|
||||
/etc/ufw \
|
||||
/etc/ollama \
|
||||
/etc/nvidia \
|
||||
/etc/profile.d
|
||||
do
|
||||
copy_path "$path"
|
||||
done
|
||||
|
||||
# Configuraciones de usuario que no suelen contener credenciales.
|
||||
while IFS=: read -r username _ uid _ _ home _; do
|
||||
[[ -d "$home" ]] || continue
|
||||
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
|
||||
for relative_path in .bashrc .profile .config/systemd/user litellm_config.yaml; do
|
||||
copy_path "$home/$relative_path"
|
||||
done
|
||||
printf '%s\t%s\t%s\t%s\n' "$username" "$uid" "$(id -g "$username" 2>/dev/null || echo 0)" "$home" \
|
||||
>> "$work_dir/metadata/users.tsv"
|
||||
done < <(getent passwd)
|
||||
|
||||
if [[ "$include_secrets" == true ]]; then
|
||||
printf 'Incluyendo archivos sensibles: SSH, certificados y configuración privada de OpenClaw.\n'
|
||||
for path in /etc/ssh /etc/ssl/private /etc/letsencrypt; do
|
||||
copy_path "$path"
|
||||
done
|
||||
while IFS=: read -r username _ uid _ _ home _; do
|
||||
[[ -d "$home" ]] || continue
|
||||
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
|
||||
for relative_path in .ssh .openclaw .npmrc; do
|
||||
copy_path "$home/$relative_path"
|
||||
done
|
||||
done < <(getent passwd)
|
||||
else
|
||||
cat > "$work_dir/metadata/excluded-secrets.txt" <<EOF
|
||||
No se incluyeron secretos. Para incluirlos explícitamente, usa:
|
||||
sudo $SCRIPT_NAME --include-secrets
|
||||
Excluidos por defecto: /etc/ssh, /etc/ssl/private, /etc/letsencrypt, ~/.ssh,
|
||||
~/.openclaw y ~/.npmrc.
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat > "$work_dir/RESTORE.txt" <<EOF
|
||||
Este archivo fue generado por $SCRIPT_NAME.
|
||||
|
||||
1. Instala Ubuntu con la misma arquitectura y crea el usuario de servicio.
|
||||
2. Copia este archivo a la nueva máquina.
|
||||
3. Ejecuta restore_ubuntu_config.sh con este archivo y revisa los cambios.
|
||||
4. Reinstala los modelos Ollama y valida los servicios antes de exponerlos.
|
||||
|
||||
El respaldo no contiene modelos Ollama, /opt/litellm-env, bases de datos ni
|
||||
logs completos. Si no usaste --include-secrets, debes restaurar las credenciales
|
||||
manualmente.
|
||||
EOF
|
||||
|
||||
printf 'Archivos incluidos:\n'
|
||||
find "$work_dir" -type f -printf '%P\n' | sort > "$work_dir/metadata/file-list.txt"
|
||||
cat "$work_dir/metadata/file-list.txt"
|
||||
|
||||
tar -C "$work_dir" -czf "$archive_path" .
|
||||
chmod 600 "$archive_path"
|
||||
printf '\nRespaldo creado: %s\n' "$archive_path"
|
||||
printf 'Protección: permisos 600\n'
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
readonly OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
|
||||
readonly CONFIG_FILE="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_HOME/openclaw.json}"
|
||||
readonly WORKSPACE_DIR="$OPENCLAW_HOME/workspace"
|
||||
readonly BACKUP_DIR="$OPENCLAW_HOME/config-backups"
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Uso: $SCRIPT_NAME
|
||||
|
||||
Corrige la configuración local de OpenClaw para este servidor:
|
||||
- usa Gateway local;
|
||||
- desactiva thinking por defecto;
|
||||
- desactiva memoria semántica sin proveedor de embeddings;
|
||||
- inicializa el workspace Git requerido por OpenClaw;
|
||||
- reinicia el Gateway de usuario;
|
||||
- verifica LiteLLM y el modelo local.
|
||||
|
||||
El script no borra sesiones, no elimina secretos y no publica archivos.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v openclaw >/dev/null 2>&1; then
|
||||
echo "Error: openclaw no está disponible en PATH." >&2
|
||||
echo "Ejecuta: source ~/.bashrc" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo "Error: no existe la configuración: $CONFIG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
backup_file="$BACKUP_DIR/openclaw.json.$(date +%Y%m%d-%H%M%S).bak"
|
||||
cp -p "$CONFIG_FILE" "$backup_file"
|
||||
chmod 600 "$backup_file"
|
||||
echo "Respaldo de configuración: $backup_file"
|
||||
|
||||
echo "Aplicando configuración local..."
|
||||
openclaw config set gateway.mode local
|
||||
openclaw config set agents.defaults.thinkingDefault off
|
||||
openclaw config set memory.search.enabled false
|
||||
|
||||
if [[ ! -d "$WORKSPACE_DIR/.git" ]]; then
|
||||
mkdir -p "$WORKSPACE_DIR"
|
||||
git -C "$WORKSPACE_DIR" init
|
||||
git -C "$WORKSPACE_DIR" config user.name "OpenClaw"
|
||||
git -C "$WORKSPACE_DIR" config user.email "openclaw@localhost"
|
||||
touch "$WORKSPACE_DIR/.gitkeep"
|
||||
git -C "$WORKSPACE_DIR" add .gitkeep
|
||||
git -C "$WORKSPACE_DIR" commit -m "Initialize OpenClaw workspace" >/dev/null
|
||||
echo "Workspace Git inicializado: $WORKSPACE_DIR"
|
||||
else
|
||||
echo "Workspace Git ya estaba inicializado: $WORKSPACE_DIR"
|
||||
fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl --user daemon-reload 2>/dev/null; then
|
||||
systemctl --user enable openclaw-gateway.service >/dev/null 2>&1 || true
|
||||
systemctl --user restart openclaw-gateway.service
|
||||
else
|
||||
echo "Aviso: no se pudo conectar al bus systemd de usuario." >&2
|
||||
echo "Cierra esta sesión SSH y vuelve a entrar como $(id -un)." >&2
|
||||
echo "Después ejecuta: systemctl --user restart openclaw-gateway.service" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Verificando configuración..."
|
||||
openclaw config get gateway.mode
|
||||
openclaw config get agents.defaults.thinkingDefault
|
||||
openclaw config get agents.defaults.model.primary
|
||||
openclaw config get models.providers.litellm.baseUrl
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if curl --fail --silent --show-error --max-time 10 \
|
||||
http://127.0.0.1:8000/v1/models | grep -q 'qwen2.5-coder:14b'; then
|
||||
echo "OK: LiteLLM publica qwen2.5-coder:14b"
|
||||
else
|
||||
echo "ERROR: LiteLLM no publica qwen2.5-coder:14b" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl --user is-active --quiet openclaw-gateway.service && \
|
||||
echo "OK: Gateway activo" || \
|
||||
echo "Aviso: Gateway no está activo; revisa openclaw gateway status" >&2
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Configuración aplicada.
|
||||
|
||||
IMPORTANTE: la sesión antigua puede seguir teniendo un historial demasiado
|
||||
largo. En la web crea una sesión nueva y prueba:
|
||||
|
||||
Responde exactamente: SESION NUEVA OK
|
||||
|
||||
No continúes usando la sesión que mostraba estimatedInput=49724 y output=1.
|
||||
|
||||
Comprobación manual:
|
||||
openclaw gateway status
|
||||
openclaw doctor
|
||||
EOF
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly INSTALLER_URL="https://openclaw.ai/install.sh"
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Uso: $SCRIPT_NAME [--onboard]
|
||||
|
||||
Instala OpenClaw usando el instalador oficial.
|
||||
|
||||
Opciones:
|
||||
--onboard Ejecuta el asistente inicial después de instalar.
|
||||
-h, --help Muestra esta ayuda.
|
||||
EOF
|
||||
}
|
||||
|
||||
onboard=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--onboard)
|
||||
onboard=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Error: opción desconocida: %s\n\n' "$1" >&2
|
||||
show_help >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
printf 'Error: este script está pensado para Linux/WSL2.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
printf 'Error: curl es obligatorio. Instálalo con: sudo apt-get update && sudo apt-get install -y curl\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
installer_file="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "$installer_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'Descargando el instalador oficial de OpenClaw...\n'
|
||||
curl --fail --silent --show-error --location \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"$INSTALLER_URL" -o "$installer_file"
|
||||
|
||||
if [[ "$onboard" == true ]]; then
|
||||
bash "$installer_file"
|
||||
else
|
||||
bash "$installer_file" --no-onboard
|
||||
fi
|
||||
|
||||
printf '\nOpenClaw se ha instalado. Comprueba la instalación con:\n'
|
||||
printf ' openclaw --version\n'
|
||||
printf ' openclaw doctor\n'
|
||||
|
||||
if [[ "$onboard" == false ]]; then
|
||||
printf '\nPara configurar OpenClaw más adelante, ejecuta:\n'
|
||||
printf ' openclaw onboard --install-daemon\n'
|
||||
fi
|
||||
@@ -0,0 +1,10 @@
|
||||
model_list:
|
||||
- model_name: qwen2.5-coder:14b
|
||||
litellm_params:
|
||||
model: ollama_chat/qwen2.5-coder:14b
|
||||
api_base: http://127.0.0.1:11434
|
||||
max_tokens: 4096
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
json_to_tool_call: true
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
archive_path=""
|
||||
confirmed=false
|
||||
install_packages=false
|
||||
start_services=false
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Uso: sudo $SCRIPT_NAME ARCHIVO.tar.gz [opciones]
|
||||
|
||||
Restaura configuraciones de un respaldo creado por backup_ubuntu_config.sh.
|
||||
Por seguridad, sin --yes solo muestra lo que se restauraría.
|
||||
|
||||
Opciones:
|
||||
--yes Ejecuta la restauración.
|
||||
--install-packages Instala los paquetes marcados como manuales en el respaldo.
|
||||
--start-services Habilita y arranca los servicios que estaban habilitados.
|
||||
-h, --help Muestra esta ayuda.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--yes)
|
||||
confirmed=true
|
||||
shift
|
||||
;;
|
||||
--install-packages)
|
||||
install_packages=true
|
||||
shift
|
||||
;;
|
||||
--start-services)
|
||||
start_services=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Opción desconocida: $1" >&2
|
||||
show_help >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
if [[ -n "$archive_path" ]]; then
|
||||
echo "Solo se admite un archivo de respaldo." >&2
|
||||
exit 2
|
||||
fi
|
||||
archive_path="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$archive_path" ]]; then
|
||||
echo "Debes indicar un archivo .tar.gz." >&2
|
||||
show_help >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Ejecuta este script con sudo o como root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ -f "$archive_path" ]] || { echo "No existe: $archive_path" >&2; exit 1; }
|
||||
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
|
||||
|
||||
work_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$work_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
tar -xzf "$archive_path" -C "$work_dir" --no-same-owner
|
||||
|
||||
if [[ ! -d "$work_dir/files" || ! -f "$work_dir/metadata/backup-info.txt" ]]; then
|
||||
echo "El archivo no parece un respaldo válido de backup_ubuntu_config.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Respaldo: %s\n' "$archive_path"
|
||||
cat "$work_dir/metadata/backup-info.txt"
|
||||
printf '\nArchivos de configuración que se restaurarían:\n'
|
||||
find "$work_dir/files" -mindepth 1 -maxdepth 8 -printf '%P\n' | sort | sed -n '1,200p'
|
||||
|
||||
if [[ "$confirmed" != true ]]; then
|
||||
cat <<EOF
|
||||
|
||||
Vista previa solamente. Para aplicar estos cambios:
|
||||
sudo $SCRIPT_NAME "$archive_path" --yes
|
||||
|
||||
Opciones adicionales:
|
||||
--install-packages Reinstala paquetes APT manuales.
|
||||
--start-services Habilita y arranca los servicios respaldados.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$install_packages" == true && -s "$work_dir/packages/apt-manual.txt" ]]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
xargs -r apt-get install -y --no-install-recommends < "$work_dir/packages/apt-manual.txt"
|
||||
fi
|
||||
|
||||
if [[ -d "$work_dir/files/etc" ]]; then
|
||||
cp -a "$work_dir/files/etc/." /etc/
|
||||
fi
|
||||
|
||||
if [[ -d "$work_dir/files/home" ]]; then
|
||||
cp -a "$work_dir/files/home/." /home/
|
||||
fi
|
||||
|
||||
if [[ -d "$work_dir/files/root" ]]; then
|
||||
cp -a "$work_dir/files/root/." /root/
|
||||
fi
|
||||
|
||||
if [[ -s "$work_dir/metadata/users.tsv" ]]; then
|
||||
while IFS=$'\t' read -r username _ _ home; do
|
||||
[[ -n "$username" && -d "$home" ]] || continue
|
||||
if id "$username" >/dev/null 2>&1; then
|
||||
chown -R "$username:$(id -gn "$username")" "$home" 2>/dev/null || true
|
||||
else
|
||||
echo "Aviso: no existe el usuario $username; revisa $home manualmente." >&2
|
||||
fi
|
||||
done < "$work_dir/metadata/users.tsv"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
if [[ "$start_services" == true && -s "$work_dir/services/enabled-service-names.txt" ]]; then
|
||||
while IFS= read -r service; do
|
||||
[[ -n "$service" ]] || continue
|
||||
systemctl enable "$service" 2>/dev/null || echo "Aviso: no se pudo habilitar $service" >&2
|
||||
done < "$work_dir/services/enabled-service-names.txt"
|
||||
|
||||
systemctl restart ollama 2>/dev/null || true
|
||||
systemctl restart litellm 2>/dev/null || true
|
||||
systemctl restart openclaw-gateway 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
ufw --force reload 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Restauración completada.
|
||||
|
||||
Revisa antes de exponer el servidor:
|
||||
systemctl --failed
|
||||
systemctl status ollama litellm
|
||||
ufw status verbose
|
||||
nvidia-smi
|
||||
|
||||
Los modelos Ollama y el entorno virtual de LiteLLM no forman parte del respaldo.
|
||||
Debes reinstalarlos o descargarlos por separado.
|
||||
EOF
|
||||
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a helpful assistant. "
|
||||
"When outputting Windows file paths in JSON arguments, you must strictly escape all backslashes "
|
||||
"(for example: C:\\\\Users\\\\name\\\\file.txt). "
|
||||
"If you need to read a file, emit a tool call with the path field."
|
||||
)
|
||||
|
||||
|
||||
def read_file(path: str) -> str:
|
||||
"""Read a file from disk and return its contents."""
|
||||
file_path = Path(path)
|
||||
try:
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
except FileNotFoundError:
|
||||
return f"ERROR: File not found: {path}"
|
||||
except Exception as exc: # pragma: no cover - demo only
|
||||
return f"ERROR: {type(exc).__name__}: {exc}"
|
||||
|
||||
|
||||
def sanitize_tool_arguments(raw_arguments: str):
|
||||
"""Repair malformed JSON emitted by the model when Windows paths are not escaped."""
|
||||
try:
|
||||
return json.loads(raw_arguments)
|
||||
except json.JSONDecodeError:
|
||||
repaired = raw_arguments.replace("\\", "\\\\")
|
||||
return json.loads(repaired)
|
||||
|
||||
|
||||
def handle_tool_call(response: dict) -> dict:
|
||||
"""Detect a tool call in the LLM response and execute it locally."""
|
||||
tool_calls = response.get("tool_calls") or response.get("function_call")
|
||||
|
||||
if tool_calls is None:
|
||||
return {"status": "final_response", "content": response}
|
||||
|
||||
if isinstance(tool_calls, dict):
|
||||
tool_calls = [tool_calls]
|
||||
|
||||
for tool_call in tool_calls:
|
||||
function_data = tool_call.get("function", tool_call)
|
||||
name = function_data.get("name")
|
||||
arguments = function_data.get("arguments", {})
|
||||
|
||||
if isinstance(arguments, str):
|
||||
try:
|
||||
arguments = json.loads(arguments)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
arguments = sanitize_tool_arguments(arguments)
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"status": "invalid_arguments",
|
||||
"raw_arguments": arguments,
|
||||
"message": "The model emitted malformed JSON. Ensure backslashes are escaped.",
|
||||
}
|
||||
|
||||
if name == "read_file":
|
||||
file_path = arguments.get("path")
|
||||
content = read_file(file_path)
|
||||
return {
|
||||
"status": "tool_result",
|
||||
"tool_call_id": tool_call.get("id"),
|
||||
"content": content,
|
||||
}
|
||||
|
||||
return {"status": "unsupported_tool_call", "raw": response}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# A valid OpenAI-style tool call with a Windows path escaped correctly.
|
||||
valid_response = {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_read_001",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path": "c:\\Workspace\\llm-server-setup\\README.md"}',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# This reproduces the common bug: malformed Windows path in JSON.
|
||||
broken_response = {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_read_002",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path": "c:\Workspace\llm-server-setup\README.md"}',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = handle_tool_call(valid_response)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
print("\n--- malformed-path fallback sample ---")
|
||||
fallback = handle_tool_call(broken_response)
|
||||
print(json.dumps(fallback, ensure_ascii=False, indent=2))
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
readonly DEFAULT_OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434}"
|
||||
readonly DEFAULT_LITELLM_URL="${LITELLM_URL:-http://127.0.0.1:8000}"
|
||||
readonly MODEL_NAME="${MODEL_NAME:-qwen2.5-coder:14b}"
|
||||
readonly OPENCLAW_CONFIG="${OPENCLAW_CONFIG:-${HOME}/.openclaw/openclaw.json}"
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
warnings=0
|
||||
report_file=""
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Uso: $SCRIPT_NAME [opciones]
|
||||
|
||||
Ejecuta un diagnóstico de solo lectura del servidor Ubuntu y de los servicios
|
||||
Ollama, LiteLLM y OpenClaw. No modifica archivos ni reinicia servicios.
|
||||
|
||||
Opciones:
|
||||
--report FILE Guarda la salida en FILE además de mostrarla en pantalla.
|
||||
-h, --help Muestra esta ayuda.
|
||||
|
||||
Variables opcionales:
|
||||
OLLAMA_URL URL de Ollama (por defecto: $DEFAULT_OLLAMA_URL)
|
||||
LITELLM_URL URL de LiteLLM (por defecto: $DEFAULT_LITELLM_URL)
|
||||
MODEL_NAME Modelo esperado (por defecto: $MODEL_NAME)
|
||||
OPENCLAW_CONFIG Ruta de configuración de OpenClaw.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--report)
|
||||
[[ $# -ge 2 ]] || { echo "Falta FILE para --report" >&2; exit 2; }
|
||||
report_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Opción desconocida: $1" >&2
|
||||
show_help >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$report_file" ]]; then
|
||||
mkdir -p "$(dirname "$report_file")"
|
||||
: > "$report_file" || { echo "No se puede escribir $report_file" >&2; exit 1; }
|
||||
exec > >(tee -a "$report_file") 2>&1
|
||||
fi
|
||||
|
||||
pass() {
|
||||
printf '[ OK ] %s\n' "$1"
|
||||
passed=$((passed + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[FAIL] %s\n' "$1"
|
||||
failed=$((failed + 1))
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[WARN] %s\n' "$1"
|
||||
warnings=$((warnings + 1))
|
||||
}
|
||||
|
||||
section() {
|
||||
printf '\n=== %s ===\n' "$1"
|
||||
}
|
||||
|
||||
has_command() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_command() {
|
||||
local command_name="$1"
|
||||
local description="$2"
|
||||
|
||||
if has_command "$command_name"; then
|
||||
pass "$description: $command_name"
|
||||
else
|
||||
fail "$description: falta $command_name"
|
||||
fi
|
||||
}
|
||||
|
||||
check_service() {
|
||||
local service="$1"
|
||||
|
||||
if ! has_command systemctl; then
|
||||
warn "No se puede consultar $service: falta systemctl"
|
||||
return
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
pass "Servicio activo: $service"
|
||||
elif systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||
warn "Servicio habilitado pero no activo: $service"
|
||||
else
|
||||
fail "Servicio no activo: $service"
|
||||
fi
|
||||
}
|
||||
|
||||
check_file() {
|
||||
local file_path="$1"
|
||||
local description="$2"
|
||||
|
||||
if [[ -e "$file_path" || -L "$file_path" ]]; then
|
||||
pass "$description: $file_path"
|
||||
else
|
||||
warn "$description no encontrado: $file_path"
|
||||
fi
|
||||
}
|
||||
|
||||
http_check() {
|
||||
local url="$1"
|
||||
local description="$2"
|
||||
local status
|
||||
|
||||
if ! has_command curl; then
|
||||
fail "$description: falta curl"
|
||||
return
|
||||
fi
|
||||
|
||||
status="$(curl --silent --show-error --max-time 10 --output /dev/null --write-out '%{http_code}' "$url" 2>/dev/null)"
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
pass "$description: HTTP $status ($url)"
|
||||
else
|
||||
fail "$description: HTTP ${status:-sin respuesta} ($url)"
|
||||
fi
|
||||
}
|
||||
|
||||
section "Sistema"
|
||||
printf 'Fecha: %s\n' "$(date --iso-8601=seconds 2>/dev/null || date)"
|
||||
printf 'Host: %s\n' "$(hostname 2>/dev/null || echo desconocido)"
|
||||
printf 'Usuario: %s\n' "$(id -un 2>/dev/null || echo desconocido)"
|
||||
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
printf 'Sistema: %s %s\n' "${NAME:-desconocido}" "${VERSION_ID:-desconocida}"
|
||||
if [[ "${ID:-}" == ubuntu ]]; then
|
||||
pass "Sistema operativo Ubuntu"
|
||||
else
|
||||
warn "El sistema operativo detectado no es Ubuntu: ${ID:-desconocido}"
|
||||
fi
|
||||
else
|
||||
fail "No se puede leer /etc/os-release"
|
||||
fi
|
||||
|
||||
check_command curl "Herramienta HTTP"
|
||||
check_command systemctl "Gestor de servicios"
|
||||
check_command ss "Inspección de puertos"
|
||||
|
||||
if has_command free; then
|
||||
free -h
|
||||
fi
|
||||
if has_command df; then
|
||||
df -hT / 2>/dev/null || true
|
||||
fi
|
||||
if has_command swapon; then
|
||||
if swapon --show --noheadings | grep -q .; then
|
||||
pass "Swap activa"
|
||||
else
|
||||
warn "No hay swap activa"
|
||||
fi
|
||||
fi
|
||||
|
||||
section "Hardware"
|
||||
if has_command nvidia-smi; then
|
||||
if nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used --format=csv,noheader 2>/dev/null; then
|
||||
pass "NVIDIA disponible"
|
||||
else
|
||||
fail "nvidia-smi no puede consultar la GPU"
|
||||
fi
|
||||
else
|
||||
warn "nvidia-smi no está instalado o la GPU NVIDIA no está disponible"
|
||||
fi
|
||||
|
||||
if [[ -r /proc/sys/vm/swappiness ]]; then
|
||||
printf 'vm.swappiness: %s\n' "$(cat /proc/sys/vm/swappiness)"
|
||||
fi
|
||||
|
||||
section "Archivos de configuración"
|
||||
check_file /etc/systemd/system/ollama.service.d/override.conf "Override de Ollama"
|
||||
check_file /etc/systemd/system/litellm.service "Servicio de LiteLLM"
|
||||
check_file /etc/systemd/system/nvidia-power-limit.service "Servicio de límite NVIDIA"
|
||||
check_file /etc/ufw "Configuración UFW"
|
||||
check_file /etc/netplan "Configuración Netplan"
|
||||
check_file "$OPENCLAW_CONFIG" "Configuración OpenClaw"
|
||||
|
||||
if [[ -f "$OPENCLAW_CONFIG" ]]; then
|
||||
if grep -Eq '"(apiKey|token|password|secret)"[[:space:]]*:' "$OPENCLAW_CONFIG"; then
|
||||
warn "La configuración OpenClaw contiene campos sensibles; no se mostrarán"
|
||||
fi
|
||||
if grep -q 'litellm/qwen2.5-coder:14b' "$OPENCLAW_CONFIG"; then
|
||||
pass "OpenClaw apunta al modelo LiteLLM esperado"
|
||||
else
|
||||
warn "No se encontró el modelo LiteLLM esperado en OpenClaw"
|
||||
fi
|
||||
fi
|
||||
|
||||
for config_path in \
|
||||
/etc/litellm_config.yaml \
|
||||
/root/litellm_config.yaml \
|
||||
"$HOME/litellm_config.yaml"
|
||||
do
|
||||
if [[ -f "$config_path" ]]; then
|
||||
printf 'LiteLLM config detectada: %s\n' "$config_path"
|
||||
if grep -q "$MODEL_NAME" "$config_path"; then
|
||||
pass "LiteLLM contiene el modelo $MODEL_NAME"
|
||||
else
|
||||
warn "LiteLLM no contiene el modelo esperado: $MODEL_NAME"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
section "Servicios"
|
||||
check_service ollama
|
||||
check_service litellm
|
||||
if has_command openclaw; then
|
||||
printf 'El Gateway de OpenClaw se validará con su propio comando de estado.\n'
|
||||
else
|
||||
warn "OpenClaw no está instalado en PATH"
|
||||
fi
|
||||
|
||||
section "Puertos"
|
||||
if has_command ss; then
|
||||
printf 'Puertos escuchando relevantes:\n'
|
||||
ss -ltnp 2>/dev/null | awk 'NR == 1 || /:8000|:11434|:18789/' || true
|
||||
if ss -ltn 2>/dev/null | grep -Eq ':11434[[:space:]]'; then
|
||||
pass "Ollama escucha en el puerto 11434"
|
||||
else
|
||||
fail "No se detecta Ollama escuchando en el puerto 11434"
|
||||
fi
|
||||
if ss -ltn 2>/dev/null | grep -Eq ':8000[[:space:]]'; then
|
||||
pass "LiteLLM escucha en el puerto 8000"
|
||||
else
|
||||
fail "No se detecta LiteLLM escuchando en el puerto 8000"
|
||||
fi
|
||||
fi
|
||||
|
||||
section "Ollama"
|
||||
http_check "$DEFAULT_OLLAMA_URL/api/tags" "API de Ollama"
|
||||
if has_command ollama; then
|
||||
if ollama list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -Fxq "$MODEL_NAME"; then
|
||||
pass "Modelo Ollama instalado: $MODEL_NAME"
|
||||
else
|
||||
fail "Modelo Ollama no encontrado: $MODEL_NAME"
|
||||
fi
|
||||
else
|
||||
warn "Comando ollama no disponible"
|
||||
fi
|
||||
|
||||
section "LiteLLM"
|
||||
http_check "$DEFAULT_LITELLM_URL/health/liveliness" "Salud de LiteLLM"
|
||||
if has_command curl; then
|
||||
models_response="$(curl --silent --show-error --max-time 10 "$DEFAULT_LITELLM_URL/v1/models" 2>/dev/null || true)"
|
||||
if [[ "$models_response" == *"$MODEL_NAME"* ]]; then
|
||||
pass "LiteLLM publica el modelo $MODEL_NAME"
|
||||
else
|
||||
fail "LiteLLM no publica el modelo $MODEL_NAME en /v1/models"
|
||||
fi
|
||||
fi
|
||||
|
||||
section "OpenClaw"
|
||||
if has_command openclaw; then
|
||||
openclaw --version 2>/dev/null || true
|
||||
if openclaw doctor 2>&1; then
|
||||
pass "openclaw doctor terminó correctamente"
|
||||
else
|
||||
fail "openclaw doctor reportó errores"
|
||||
fi
|
||||
if openclaw gateway status 2>&1; then
|
||||
pass "Estado del Gateway consultado correctamente"
|
||||
else
|
||||
warn "No se pudo consultar correctamente el Gateway"
|
||||
fi
|
||||
else
|
||||
warn "No se puede probar OpenClaw porque no está instalado"
|
||||
fi
|
||||
|
||||
section "Firewall"
|
||||
if has_command ufw; then
|
||||
ufw status verbose 2>/dev/null || true
|
||||
if ufw status 2>/dev/null | grep -qi active; then
|
||||
pass "UFW está activo"
|
||||
else
|
||||
warn "UFW no está activo"
|
||||
fi
|
||||
else
|
||||
warn "UFW no está instalado"
|
||||
fi
|
||||
|
||||
section "Resumen"
|
||||
printf 'Correctos: %d | Fallos: %d | Advertencias: %d\n' "$passed" "$failed" "$warnings"
|
||||
if [[ -n "$report_file" ]]; then
|
||||
printf 'Reporte: %s\n' "$report_file"
|
||||
fi
|
||||
|
||||
if [[ "$failed" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user