Python es la opción natural cuando un script de administración deja de ser una lista de comandos: procesar logs, hacer peticiones HTTP en paralelo o trabajar con datos estructurados es mucho más legible que en Bash. En este tutorial crearás en Ubuntu 24.04 tres scripts útiles: una comprobación de CPU, memoria y disco con psutil, un analizador de logs de Nginx y un comprobador de URL. Los instalarás en un entorno virtual y programarás uno de ellos con un temporizador de systemd.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, que incluye Python 3.12, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Nginx instalado si quieres probar el analizador de logs con datos reales (es opcional).
Paso 1: Crear un entorno virtual para los scripts
En Ubuntu 24.04 no puedes instalar paquetes con pip en el Python del sistema. Aunque instales python3-pip, un pip install fuera de un entorno virtual termina con este error:
error: externally-managed-environment
Es una protección intencionada (PEP 668) para que pip no rompa paquetes que gestiona apt. La solución correcta es un entorno virtual. Instala el módulo venv:
sudo apt update
sudo apt install python3-venv
Crea un directorio para los scripts y un entorno virtual dentro de él:
sudo mkdir -p /opt/ops-scripts
sudo python3 -m venv /opt/ops-scripts/venv
Instala psutil, la biblioteca que da acceso multiplataforma a CPU, memoria, discos y procesos:
sudo /opt/ops-scripts/venv/bin/pip install psutil
Comprueba que el entorno virtual la encuentra:
/opt/ops-scripts/venv/bin/python -c "import psutil; print(psutil.__version__)"
7.2.2
El número de versión puede ser distinto. Lo importante es que no aparezca ModuleNotFoundError.
Paso 2: Comprobar CPU, memoria y disco con psutil
El primer script mide el uso de CPU, memoria y cada sistema de archivos, y avisa cuando se supera un umbral. Termina con código 1 si hay algún aviso, lo que permite detectarlo desde systemd o desde cualquier otro script. Crea el archivo:
sudo nano /opt/ops-scripts/check_resources.py
#!/opt/ops-scripts/venv/bin/python
"""Comprueba CPU, memoria y disco y avisa si se supera un umbral."""
import argparse
import logging
import sys
import psutil
# Sistemas de archivos virtuales o de solo lectura que no interesa vigilar
IGNORED_FS = {"squashfs", "overlay", "tmpfs", "devtmpfs"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cpu", type=float, default=90.0, help="umbral de CPU en %%")
parser.add_argument("--mem", type=float, default=90.0, help="umbral de memoria en %%")
parser.add_argument("--disk", type=float, default=85.0, help="umbral de disco en %%")
return parser.parse_args()
def main() -> int:
args = parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
checks = [
("CPU", psutil.cpu_percent(interval=1), args.cpu),
("Memoria", psutil.virtual_memory().percent, args.mem),
]
for part in psutil.disk_partitions():
if part.fstype in IGNORED_FS:
continue
usage = psutil.disk_usage(part.mountpoint)
checks.append((f"Disco {part.mountpoint}", usage.percent, args.disk))
problems = 0
for name, value, limit in checks:
if value >= limit:
logging.warning("%s al %.1f%% (umbral %.0f%%)", name, value, limit)
problems += 1
else:
logging.info("%s al %.1f%%", name, value)
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main())
La primera línea (shebang) apunta al Python del entorno virtual, así que el script encuentra psutil sin tener que activar nada. cpu_percent(interval=1) mide durante un segundo; sin intervalo, la primera llamada siempre devuelve 0.
Hazlo ejecutable y pruébalo:
sudo chmod 755 /opt/ops-scripts/check_resources.py
/opt/ops-scripts/check_resources.py
INFO CPU al 3.0%
INFO Memoria al 21.4%
INFO Disco / al 23.1%
INFO Disco /boot al 12.0%
Fuerza un aviso con un umbral bajo y comprueba el código de salida:
/opt/ops-scripts/check_resources.py --mem 1; echo "código de salida: $?"
INFO CPU al 2.5%
WARNING Memoria al 21.4% (umbral 1%)
INFO Disco / al 23.1%
INFO Disco /boot al 12.0%
código de salida: 1
Paso 3: Analizar los logs de acceso de Nginx
El segundo script lee un access.log de Nginx en el formato por defecto (combined) y resume qué códigos de estado se devuelven, qué IP hacen más peticiones y qué rutas dan más errores 404. Solo usa la biblioteca estándar y acepta también logs rotados comprimidos con gzip. Crea el archivo:
sudo nano /opt/ops-scripts/nginx_summary.py
#!/usr/bin/env python3
"""Resume un access.log de Nginx en formato combined."""
import argparse
import gzip
import re
from collections import Counter
from pathlib import Path
LINE_RE = re.compile(
r'(?P<ip>\S+) \S+ \S+ \[[^\]]+\] '
r'"(?P<method>[A-Z]+) (?P<path>\S+) [^"]*" (?P<status>\d{3}) '
)
def open_log(path: Path):
if path.suffix == ".gz":
return gzip.open(path, "rt", encoding="utf-8", errors="replace")
return path.open(encoding="utf-8", errors="replace")
def print_top(title: str, counter: Counter, top: int) -> None:
print(f"\n{title}")
for key, count in counter.most_common(top):
print(f" {count:>8} {key}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("logfile", type=Path, nargs="?",
default=Path("/var/log/nginx/access.log"))
parser.add_argument("--top", type=int, default=10, help="filas por sección")
args = parser.parse_args()
statuses, ips, not_found = Counter(), Counter(), Counter()
total = skipped = 0
with open_log(args.logfile) as log:
for line in log:
match = LINE_RE.match(line)
if not match:
skipped += 1
continue
total += 1
statuses[match["status"]] += 1
ips[match["ip"]] += 1
if match["status"] == "404":
not_found[match["path"]] += 1
print(f"Peticiones analizadas: {total} (líneas ignoradas: {skipped})")
print_top("Códigos de estado", statuses, args.top)
print_top("IP con más peticiones", ips, args.top)
print_top("Rutas con más 404", not_found, args.top)
if __name__ == "__main__":
main()
Los logs de Nginx pertenecen al grupo adm, al que tu usuario ya pertenece en Ubuntu si fue el primero que se creó. Hazlo ejecutable y analiza el log actual:
sudo chmod 755 /opt/ops-scripts/nginx_summary.py
/opt/ops-scripts/nginx_summary.py --top 3
Peticiones analizadas: 18422 (líneas ignoradas: 12)
Códigos de estado
15210 200
2104 404
871 301
IP con más peticiones
3120 203.0.113.24
1985 198.51.100.7
644 192.0.2.61
Rutas con más 404
412 /wp-login.php
305 /.env
118 /favicon.ico
Las líneas ignoradas suelen ser peticiones malformadas de escáneres, que Nginx registra con la petición "-". Para analizar un log rotado, pásale la ruta: /opt/ops-scripts/nginx_summary.py /var/log/nginx/access.log.2.gz.
Paso 4: Comprobar que tus URL responden
El tercer script comprueba en paralelo una lista de URL y muestra el código de estado y el tiempo de respuesta de cada una. Usa urllib de la biblioteca estándar y un ThreadPoolExecutor, de modo que diez URL lentas tardan lo mismo que una. Crea el archivo:
sudo nano /opt/ops-scripts/check_urls.py
#!/usr/bin/env python3
"""Comprueba que una lista de URL responde sin errores."""
import argparse
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
def check(url: str, timeout: float) -> tuple[str, int | None, str]:
request = urllib.request.Request(url, headers={"User-Agent": "check-urls/1.0"})
start = time.monotonic()
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
status = response.status
except urllib.error.HTTPError as exc:
status = exc.code
except (urllib.error.URLError, TimeoutError, OSError) as exc:
return url, None, str(getattr(exc, "reason", exc))
elapsed_ms = (time.monotonic() - start) * 1000
return url, status, f"{elapsed_ms:.0f} ms"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("urlfile", type=Path, help="archivo con una URL por línea")
parser.add_argument("--timeout", type=float, default=10.0)
args = parser.parse_args()
urls = [
line.strip()
for line in args.urlfile.read_text().splitlines()
if line.strip() and not line.startswith("#")
]
failed = 0
with ThreadPoolExecutor(max_workers=10) as pool:
for url, status, detail in pool.map(lambda u: check(u, args.timeout), urls):
ok = status is not None and status < 400
failed += not ok
print(f"{'OK ' if ok else 'FAIL'} {status or '---'} {url} ({detail})")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
urlopen sigue las redirecciones, así que un 301 hacia HTTPS se muestra con el código final. Los errores HTTP (4xx y 5xx) llegan como HTTPError y se registran con su código; los fallos de red o DNS se muestran sin código.
Crea la lista de URL. Sustituye your_domain por tu dominio:
sudo nano /opt/ops-scripts/urls.txt
# Una URL por línea
https://your_domain/
https://your_domain/api/health
https://www.cubepath.com/
Hazlo ejecutable y pruébalo:
sudo chmod 755 /opt/ops-scripts/check_urls.py
/opt/ops-scripts/check_urls.py /opt/ops-scripts/urls.txt
OK 200 https://your_domain/ (84 ms)
FAIL 404 https://your_domain/api/health (61 ms)
OK 200 https://www.cubepath.com/ (143 ms)
Paso 5: Programar la comprobación de recursos con systemd
Un temporizador de systemd ejecutará check_resources.py cada cinco minutos. La salida irá al journal y, cuando se supere un umbral, el servicio quedará en estado failed. Crea la unidad de servicio:
sudo nano /etc/systemd/system/check-resources.service
[Unit]
Description=Comprobar CPU, memoria y disco
[Service]
Type=oneshot
ExecStart=/opt/ops-scripts/check_resources.py --cpu 90 --mem 90 --disk 85
DynamicUser=yes
DynamicUser=yes ejecuta el script con un usuario temporal sin privilegios, suficiente porque solo lee métricas del sistema.
Crea el temporizador:
sudo nano /etc/systemd/system/check-resources.timer
[Unit]
Description=Comprobar recursos cada 5 minutos
[Timer]
OnCalendar=*:0/5
[Install]
WantedBy=timers.target
Recarga systemd, activa el temporizador y lanza una ejecución manual para comprobarlo:
sudo systemctl daemon-reload
sudo systemctl enable --now check-resources.timer
sudo systemctl start check-resources.service
journalctl -u check-resources.service -n 6 --no-pager
sep 25 11:05:12 servidor systemd[1]: Starting check-resources.service - Comprobar CPU, memoria y disco...
sep 25 11:05:13 servidor check_resources.py[6012]: INFO CPU al 2.0%
sep 25 11:05:13 servidor check_resources.py[6012]: INFO Memoria al 21.6%
sep 25 11:05:13 servidor check_resources.py[6012]: INFO Disco / al 23.1%
sep 25 11:05:13 servidor systemd[1]: check-resources.service: Deactivated successfully.
sep 25 11:05:13 servidor systemd[1]: Finished check-resources.service - Comprobar CPU, memoria y disco.
Para ver solo los avisos de los últimos días, filtra por prioridad: journalctl -u check-resources.service -p warning --since "2 days ago".
Solución de problemas
error: externally-managed-environment al usar pip: estás instalando en el Python del sistema. Usa siempre el pip del entorno virtual (/opt/ops-scripts/venv/bin/pip). Si solo necesitas una biblioteca empaquetada por Ubuntu, también puedes instalarla con apt, por ejemplo sudo apt install python3-psutil.
ModuleNotFoundError: No module named 'psutil': el script se está ejecutando con /usr/bin/python3 en lugar del Python del entorno virtual. Revisa el shebang o ejecútalo con /opt/ops-scripts/venv/bin/python script.py.
PermissionError al leer /var/log/nginx/access.log: tu usuario no está en el grupo adm. Añádelo con sudo usermod -aG adm $USER y abre una sesión nueva.
El entorno virtual deja de funcionar tras actualizar Ubuntu: los entornos virtuales dependen de la versión exacta de Python. Tras cambiar de versión mayor, recréalo con sudo python3 -m venv --clear /opt/ops-scripts/venv y reinstala las dependencias.
Conclusión
Has creado un entorno virtual aislado del Python del sistema, tres scripts de administración (recursos con psutil, análisis de logs de Nginx y comprobación de URL) y un temporizador de systemd que vigila los recursos cada cinco minutos.
Como siguientes pasos puedes:
- Guardar las dependencias en un
requirements.txty versionar/opt/ops-scriptsen Git para desplegarlo igual en todos tus servidores. - Programar
check_urls.pycon otro temporizador y enviar los fallos a un canal de chat mediante un webhook. - Usar Fabric o Ansible cuando necesites ejecutar estas comprobaciones en varios servidores a la vez.
