6a05abff03
Tres pipelines Python para gestionar operations.db en Metabase: - metabase_add_ops_db: registra la operations.db de una app como database SQLite - metabase_create_ops_dashboard: genera dashboard operativo con 14 cards (KPIs, distribución, executions, assertions) para cualquier app - metabase_fix_permissions: arregla SQLITE_READONLY_DIRECTORY haciendo chmod 777/666 sin chown (que se propaga al host via bind mount) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
"""Pipeline: arregla permisos de SQLite en el contenedor Metabase.
|
|
|
|
Compone: metabase_auth + metabase_list_databases (para verificar).
|
|
|
|
SQLite necesita escribir journal/WAL files en el mismo directorio que la BD.
|
|
El contenedor Metabase corre Java como UID 2000 (usuario metabase).
|
|
Si los directorios o archivos no son escribibles por ese UID, Metabase
|
|
devuelve SQLITE_READONLY_DIRECTORY.
|
|
|
|
Este pipeline:
|
|
1. Detecta el contenedor Metabase
|
|
2. Encuentra todos los directorios /data/ que contienen .db
|
|
3. Hace chmod 777 en directorios y chmod 666 en archivos .db
|
|
4. Verifica que Metabase puede leer cada database
|
|
|
|
Uso:
|
|
python metabase_fix_permissions.py
|
|
python metabase_fix_permissions.py --container fn_registry-metabase
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from metabase.client import metabase_auth
|
|
from metabase import metabase_list_databases
|
|
|
|
METABASE_URL = os.environ.get("METABASE_URL", "http://localhost:3000")
|
|
EMAIL = os.environ.get("METABASE_ADMIN_EMAIL", "admin@fnregistry.local")
|
|
PASSWORD = os.environ.get("METABASE_ADMIN_PASSWORD", "FnRegistry2024!")
|
|
DEFAULT_CONTAINER = "fn_registry-metabase"
|
|
|
|
|
|
def docker_exec(container: str, cmd: list[str], user: str = "root") -> str:
|
|
"""Ejecuta comando dentro del contenedor."""
|
|
result = subprocess.run(
|
|
["docker", "exec", "-u", user, container] + cmd,
|
|
capture_output=True, text=True,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def fix_container_permissions(container: str) -> list[str]:
|
|
"""Arregla permisos de todos los .db en /data/ del contenedor.
|
|
|
|
Returns:
|
|
Lista de paths arreglados.
|
|
"""
|
|
# Encontrar todos los .db bajo /data/
|
|
find_output = docker_exec(container, ["find", "/data", "-name", "*.db", "-type", "f"])
|
|
if not find_output:
|
|
print(" No se encontraron archivos .db en /data/")
|
|
return []
|
|
|
|
db_files = [f.strip() for f in find_output.splitlines() if f.strip()]
|
|
fixed = []
|
|
|
|
for db_path in db_files:
|
|
db_dir = os.path.dirname(db_path)
|
|
|
|
# Fix directorio: 777 para que UID 2000 pueda crear journal
|
|
docker_exec(container, ["chmod", "777", db_dir])
|
|
|
|
# Fix archivo: 666 para que UID 2000 pueda leer/escribir
|
|
docker_exec(container, ["chmod", "666", db_path])
|
|
|
|
# Fix WAL/SHM si existen
|
|
for suffix in ["-wal", "-shm", "-journal"]:
|
|
wal_path = db_path + suffix
|
|
docker_exec(container, ["chmod", "666", wal_path])
|
|
|
|
fixed.append(db_path)
|
|
print(f" Fixed: {db_path} (dir: {db_dir})")
|
|
|
|
return fixed
|
|
|
|
|
|
def verify_databases(client) -> bool:
|
|
"""Verifica que todas las databases SQLite responden."""
|
|
import httpx
|
|
|
|
dbs = metabase_list_databases(client)
|
|
all_ok = True
|
|
|
|
for db in dbs:
|
|
if db.get("engine") != "sqlite":
|
|
continue
|
|
|
|
name = db["name"]
|
|
db_id = db["id"]
|
|
|
|
try:
|
|
resp = client._http.request("POST", "/api/dataset", json={
|
|
"database": db_id,
|
|
"type": "native",
|
|
"native": {"query": "SELECT 1"},
|
|
}, extensions={"timeout": {"read": 60, "write": 10, "connect": 10, "pool": 10}})
|
|
|
|
if resp.status_code in (200, 202):
|
|
print(f" OK: {name} (id={db_id})")
|
|
else:
|
|
error = resp.text[:150]
|
|
print(f" FAIL: {name} (id={db_id}) -> {error}")
|
|
all_ok = False
|
|
except httpx.ReadTimeout:
|
|
print(f" TIMEOUT: {name} (id={db_id}) -> Metabase aun sincronizando, reintentar en unos segundos")
|
|
all_ok = False
|
|
|
|
return all_ok
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Arregla permisos SQLite en contenedor Metabase")
|
|
parser.add_argument("--container", default=DEFAULT_CONTAINER, help="Nombre del contenedor Metabase")
|
|
args = parser.parse_args()
|
|
|
|
container = args.container
|
|
|
|
# Verificar que el contenedor existe
|
|
result = subprocess.run(
|
|
["docker", "inspect", container, "--format", "{{.State.Running}}"],
|
|
capture_output=True, text=True,
|
|
)
|
|
if result.returncode != 0 or result.stdout.strip() != "true":
|
|
print(f"ERROR: Contenedor '{container}' no esta corriendo.")
|
|
sys.exit(1)
|
|
|
|
print(f"Contenedor: {container}")
|
|
print(f"\n1. Arreglando permisos en /data/...")
|
|
fixed = fix_container_permissions(container)
|
|
|
|
if not fixed:
|
|
print(" Nada que arreglar.")
|
|
return
|
|
|
|
print(f"\n2. Verificando databases en Metabase...")
|
|
client = metabase_auth(METABASE_URL, EMAIL, PASSWORD)
|
|
ok = verify_databases(client)
|
|
client.close()
|
|
|
|
if ok:
|
|
print(f"\nTodas las databases responden correctamente.")
|
|
else:
|
|
print(f"\nAlgunas databases siguen fallando. Revisar logs del contenedor:")
|
|
print(f" docker logs {container} --tail 50")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|