diff --git a/bash/functions/infra/systemd_local_enable.md b/bash/functions/infra/systemd_local_enable.md new file mode 100644 index 00000000..0a9baa60 --- /dev/null +++ b/bash/functions/infra/systemd_local_enable.md @@ -0,0 +1,38 @@ +--- +name: systemd_local_enable +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_enable(name: string) -> json" +description: "Habilita un servicio systemd local con systemctl enable para que arranque automáticamente al boot. Requiere sudo." +tags: [systemd, service, local, infra, enable] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo .service" +output: "JSON {name, enabled:true}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_enable.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_enable.sh +systemd_local_enable "sqlite_api" +# {"name":"sqlite_api","enabled":true} +``` + +## Notas + +- El unit debe existir en `/etc/systemd/system/` (usar `systemd_local_install_unit` primero). +- No arranca el servicio — solo lo habilita para el próximo boot. Usar `systemd_local_start` para lanzarlo ahora. diff --git a/bash/functions/infra/systemd_local_enable.sh b/bash/functions/infra/systemd_local_enable.sh new file mode 100644 index 00000000..1925c7c3 --- /dev/null +++ b/bash/functions/infra/systemd_local_enable.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# systemd_local_enable — Habilita un servicio systemd local (arranque automático). +set -euo pipefail + +systemd_local_enable() { + local name="$1" + + if [[ -z "$name" ]]; then + echo "systemd_local_enable: se requiere name" >&2 + return 1 + fi + + # systemctl enable imprime "Created symlink ..." en stdout — redirigir a stderr + # para que $(systemd_local_enable ...) capture sólo el JSON final. + if ! sudo systemctl enable "${name}.service" >&2; then + echo "systemd_local_enable: enable falló para '$name'" >&2 + return 1 + fi + + printf '{"name":"%s","enabled":true}\n' "$name" +} diff --git a/bash/functions/infra/systemd_local_install_unit.md b/bash/functions/infra/systemd_local_install_unit.md new file mode 100644 index 00000000..0c334533 --- /dev/null +++ b/bash/functions/infra/systemd_local_install_unit.md @@ -0,0 +1,58 @@ +--- +name: systemd_local_install_unit +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_install_unit(name: string, unit_content: string) -> json" +description: "Instala un unit file de systemd en /etc/systemd/system/.service y ejecuta daemon-reload. Requiere sudo sin password para install y systemctl. Sobrescribe si el unit ya existe." +tags: [systemd, service, local, infra, wsl] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo (se añade .service automáticamente)" + - name: unit_content + desc: "contenido completo del archivo unit como texto (con secciones [Unit], [Service], [Install])" +output: "JSON {name, path, installed:true}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_install_unit.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_install_unit.sh + +unit=$(cat <<'EOF' +[Unit] +Description=my service +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/my_service +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF +) + +systemd_local_install_unit "my_service" "$unit" +# {"name":"my_service","path":"/etc/systemd/system/my_service.service","installed":true} +``` + +## Notas + +- Usa `install -m 0644 -o root -g root` para escribir el unit con permisos correctos. +- Llama a `sudo systemctl daemon-reload` al final — imprescindible para que systemd vea el unit. +- No hace `enable` ni `start` — esas son funciones separadas (principio de composabilidad). +- En WSL requiere `systemd=true` en `/etc/wsl.conf`. diff --git a/bash/functions/infra/systemd_local_install_unit.sh b/bash/functions/infra/systemd_local_install_unit.sh new file mode 100644 index 00000000..1546e5a7 --- /dev/null +++ b/bash/functions/infra/systemd_local_install_unit.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# systemd_local_install_unit — Instala un unit file en /etc/systemd/system y recarga systemd. +set -euo pipefail + +systemd_local_install_unit() { + local name="$1" + local unit_content="$2" + + if [[ -z "$name" || -z "$unit_content" ]]; then + echo "systemd_local_install_unit: se requieren name y unit_content" >&2 + return 1 + fi + + local unit_path="/etc/systemd/system/${name}.service" + local tmp + tmp="$(mktemp)" + printf '%s' "$unit_content" > "$tmp" + + if ! sudo install -m 0644 -o root -g root "$tmp" "$unit_path"; then + rm -f "$tmp" + echo "systemd_local_install_unit: no se pudo instalar '$unit_path'" >&2 + return 1 + fi + rm -f "$tmp" + + if ! sudo systemctl daemon-reload; then + echo "systemd_local_install_unit: daemon-reload falló" >&2 + return 1 + fi + + printf '{"name":"%s","path":"%s","installed":true}\n' "$name" "$unit_path" +} diff --git a/bash/functions/infra/systemd_local_restart.md b/bash/functions/infra/systemd_local_restart.md new file mode 100644 index 00000000..208cdbd1 --- /dev/null +++ b/bash/functions/infra/systemd_local_restart.md @@ -0,0 +1,38 @@ +--- +name: systemd_local_restart +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_restart(name: string) -> json" +description: "Reinicia un servicio systemd local con systemctl restart. Útil tras actualizar el binario o cambiar el unit. Requiere sudo." +tags: [systemd, service, local, infra, restart] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo .service" +output: "JSON {name, restarted:true, pid:int}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_restart.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_restart.sh +systemd_local_restart "sqlite_api" +# {"name":"sqlite_api","restarted":true,"pid":54321} +``` + +## Notas + +- Si modificaste el unit, primero ejecuta `sudo systemctl daemon-reload` (o llama a `systemd_local_install_unit` que ya lo hace). +- Equivalente a stop+start pero systemd lo gestiona atómicamente. diff --git a/bash/functions/infra/systemd_local_restart.sh b/bash/functions/infra/systemd_local_restart.sh new file mode 100644 index 00000000..d5cd90be --- /dev/null +++ b/bash/functions/infra/systemd_local_restart.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# systemd_local_restart — Reinicia un servicio systemd local. +set -euo pipefail + +systemd_local_restart() { + local name="$1" + + if [[ -z "$name" ]]; then + echo "systemd_local_restart: se requiere name" >&2 + return 1 + fi + + if ! sudo systemctl restart "${name}.service" >&2; then + echo "systemd_local_restart: restart falló para '$name'" >&2 + return 1 + fi + + local pid + pid=$(systemctl show -p MainPID --value "${name}.service" 2>/dev/null || echo 0) + + printf '{"name":"%s","restarted":true,"pid":%s}\n' "$name" "${pid:-0}" +} diff --git a/bash/functions/infra/systemd_local_start.md b/bash/functions/infra/systemd_local_start.md new file mode 100644 index 00000000..355b989e --- /dev/null +++ b/bash/functions/infra/systemd_local_start.md @@ -0,0 +1,38 @@ +--- +name: systemd_local_start +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_start(name: string) -> json" +description: "Arranca un servicio systemd local con systemctl start. Devuelve el MainPID asignado. Requiere sudo." +tags: [systemd, service, local, infra, start] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo .service" +output: "JSON {name, started:true, pid:int}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_start.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_start.sh +systemd_local_start "sqlite_api" +# {"name":"sqlite_api","started":true,"pid":12345} +``` + +## Notas + +- Si el servicio ya está corriendo, `systemctl start` es idempotente (no hace nada). +- El PID devuelto es el `MainPID` según systemd. 0 si el arranque falló en silencio (usar `systemd_local_status` para diagnóstico). diff --git a/bash/functions/infra/systemd_local_start.sh b/bash/functions/infra/systemd_local_start.sh new file mode 100644 index 00000000..54566aff --- /dev/null +++ b/bash/functions/infra/systemd_local_start.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# systemd_local_start — Arranca un servicio systemd local. +set -euo pipefail + +systemd_local_start() { + local name="$1" + + if [[ -z "$name" ]]; then + echo "systemd_local_start: se requiere name" >&2 + return 1 + fi + + if ! sudo systemctl start "${name}.service" >&2; then + echo "systemd_local_start: start falló para '$name'" >&2 + return 1 + fi + + local pid + pid=$(systemctl show -p MainPID --value "${name}.service" 2>/dev/null || echo 0) + + printf '{"name":"%s","started":true,"pid":%s}\n' "$name" "${pid:-0}" +} diff --git a/bash/functions/infra/systemd_local_status.md b/bash/functions/infra/systemd_local_status.md new file mode 100644 index 00000000..d0262800 --- /dev/null +++ b/bash/functions/infra/systemd_local_status.md @@ -0,0 +1,47 @@ +--- +name: systemd_local_status +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_status(name: string, log_lines: int = 10) -> json" +description: "Devuelve el estado de un servicio systemd local: active state, sub state, PID, enabled, y las N últimas líneas de journalctl. No requiere sudo." +tags: [systemd, service, local, infra, status, journalctl] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo .service" + - name: log_lines + desc: "número de líneas de log a incluir en el JSON (default 10)" +output: "JSON {name, active, sub, enabled, pid, logs:[...]}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_status.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_status.sh +systemd_local_status "sqlite_api" 5 +# {"name":"sqlite_api","active":"active","sub":"running","enabled":"enabled","pid":12345,"logs":["...","..."]} +``` + +## Valores típicos + +- `active`: `active`, `inactive`, `failed`, `activating`, `deactivating` +- `sub`: `running`, `dead`, `exited`, `start-pre`, etc. +- `enabled`: `enabled`, `disabled`, `static`, `masked` + +## Notas + +- No requiere sudo (solo lectura). +- Si el unit no existe, `active` será `inactive` y `sub` será `dead`. +- Los logs vienen de `journalctl -u -n N --no-pager -o cat` (sin prefijos, solo el mensaje). diff --git a/bash/functions/infra/systemd_local_status.sh b/bash/functions/infra/systemd_local_status.sh new file mode 100644 index 00000000..464591d2 --- /dev/null +++ b/bash/functions/infra/systemd_local_status.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# systemd_local_status — Estado + últimos logs de un servicio systemd local. +set -euo pipefail + +systemd_local_status() { + local name="$1" + local log_lines="${2:-10}" + + if [[ -z "$name" ]]; then + echo "systemd_local_status: se requiere name" >&2 + return 1 + fi + + local active sub pid enabled + active=$(systemctl show -p ActiveState --value "${name}.service" 2>/dev/null || echo unknown) + sub=$(systemctl show -p SubState --value "${name}.service" 2>/dev/null || echo unknown) + pid=$(systemctl show -p MainPID --value "${name}.service" 2>/dev/null || echo 0) + enabled=$(systemctl is-enabled "${name}.service" 2>/dev/null || echo disabled) + + # logs como array JSON + local logs_json + logs_json=$(journalctl -u "${name}.service" -n "$log_lines" --no-pager -o cat 2>/dev/null \ + | python3 -c 'import sys, json; print(json.dumps([l.rstrip() for l in sys.stdin if l.strip()]))' \ + 2>/dev/null || echo "[]") + + printf '{"name":"%s","active":"%s","sub":"%s","enabled":"%s","pid":%s,"logs":%s}\n' \ + "$name" "$active" "$sub" "$enabled" "${pid:-0}" "$logs_json" +} diff --git a/bash/functions/infra/systemd_local_uninstall.md b/bash/functions/infra/systemd_local_uninstall.md new file mode 100644 index 00000000..321efefb --- /dev/null +++ b/bash/functions/infra/systemd_local_uninstall.md @@ -0,0 +1,39 @@ +--- +name: systemd_local_uninstall +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "systemd_local_uninstall(name: string) -> json" +description: "Detiene, deshabilita y elimina el unit file de un servicio systemd local. Idempotente: no falla si el servicio ya está parado o el unit no existe. Requiere sudo." +tags: [systemd, service, local, infra, uninstall, cleanup] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "nombre del servicio sin sufijo .service" +output: "JSON {name, uninstalled:true}. Errores a stderr, exit 1." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/systemd_local_uninstall.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/systemd_local_uninstall.sh +systemd_local_uninstall "sqlite_api" +# {"name":"sqlite_api","uninstalled":true} +``` + +## Notas + +- Secuencia: stop → disable → rm unit → daemon-reload → reset-failed. +- `stop` y `disable` con `|| true` para idempotencia (si ya no estaba corriendo/enabled, no es error). +- `reset-failed` limpia el estado "failed" si el servicio había fallado previamente. diff --git a/bash/functions/infra/systemd_local_uninstall.sh b/bash/functions/infra/systemd_local_uninstall.sh new file mode 100644 index 00000000..f5eeae92 --- /dev/null +++ b/bash/functions/infra/systemd_local_uninstall.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# systemd_local_uninstall — Detiene, deshabilita y elimina un servicio systemd local. +set -euo pipefail + +systemd_local_uninstall() { + local name="$1" + + if [[ -z "$name" ]]; then + echo "systemd_local_uninstall: se requiere name" >&2 + return 1 + fi + + local unit_path="/etc/systemd/system/${name}.service" + + # stop (idempotente: no falla si ya parado) + sudo systemctl stop "${name}.service" 2>/dev/null || true + sudo systemctl disable "${name}.service" 2>/dev/null || true + + if [[ -f "$unit_path" ]]; then + sudo rm -f "$unit_path" + fi + + sudo systemctl daemon-reload + sudo systemctl reset-failed "${name}.service" 2>/dev/null || true + + printf '{"name":"%s","uninstalled":true}\n' "$name" +} diff --git a/bash/functions/pipelines/install_systemd_service.md b/bash/functions/pipelines/install_systemd_service.md new file mode 100644 index 00000000..0850812e --- /dev/null +++ b/bash/functions/pipelines/install_systemd_service.md @@ -0,0 +1,80 @@ +--- +name: install_systemd_service +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "install_systemd_service --name --exec [opts] -> json" +description: "Pipeline que registra una app como servicio systemd del sistema: genera el unit, lo instala en /etc/systemd/system/, hace daemon-reload, enable, start y devuelve status. Requiere sudo sin password para systemctl y escritura en /etc/systemd/system/." +tags: [systemd, service, local, infra, pipeline, install] +uses_functions: + - systemd_local_install_unit_bash_infra + - systemd_local_enable_bash_infra + - systemd_local_start_bash_infra + - systemd_local_status_bash_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: --name + desc: "nombre del servicio (sin sufijo .service)" + - name: --exec + desc: "ruta absoluta al binario/script para ExecStart=" + - name: --workdir + desc: "WorkingDirectory (default: dirname del --exec)" + - name: --user + desc: "User del servicio (default: usuario actual, id -un)" + - name: --description + desc: "Description del unit (default: ' service')" + - name: --env + desc: "Variable de entorno en formato KEY=VAL (repetible)" + - name: --after + desc: "After= del unit (default: network.target)" + - name: --restart + desc: "Restart= del unit (default: on-failure)" + - name: --type + desc: "Type= del unit (default: simple)" +output: "JSON consolidado con subkeys install, enable, start y status de cada paso del pipeline." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/pipelines/install_systemd_service.sh" +--- + +## Ejemplo + +```bash +source bash/functions/pipelines/install_systemd_service.sh + +install_systemd_service \ + --name sqlite_api \ + --exec /home/egutierrez/fn_registry/projects/fn_monitoring/apps/sqlite_api/sqlite_api \ + --workdir /home/egutierrez/fn_registry/projects/fn_monitoring/apps/sqlite_api \ + --env FN_REGISTRY_ROOT=/home/egutierrez/fn_registry \ + --description "fn_registry sqlite_api (read-only HTTP API)" +``` + +Salida (resumida): +```json +{ + "install": {"name":"sqlite_api","path":"/etc/systemd/system/sqlite_api.service","installed":true}, + "enable": {"name":"sqlite_api","enabled":true}, + "start": {"name":"sqlite_api","started":true,"pid":12345}, + "status": {"name":"sqlite_api","active":"active","sub":"running","enabled":"enabled","pid":12345,"logs":["..."]} +} +``` + +## Requisitos + +- `systemd` activo en la máquina (en WSL: `systemd=true` en `/etc/wsl.conf`). +- `sudo` sin password para `systemctl` y escritura en `/etc/systemd/system/`. + +## Notas + +- Idempotente: si el unit ya existe, se sobrescribe y systemd se recarga. +- Para desinstalar usar `systemd_local_uninstall `. +- Orden determinista de Environment= (uno por línea, en el orden pasado en CLI). +- El pipeline NO compila el binario — se asume que `--exec` apunta a un ejecutable ya listo. diff --git a/bash/functions/pipelines/install_systemd_service.sh b/bash/functions/pipelines/install_systemd_service.sh new file mode 100644 index 00000000..09ded4cc --- /dev/null +++ b/bash/functions/pipelines/install_systemd_service.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# install_systemd_service — Pipeline que registra una app como servicio systemd local. +# Compone systemd_local_{install_unit, enable, start, status}. +set -euo pipefail + +# Resolver repo root (asume que este archivo vive en bash/functions/pipelines/) +PIPELINE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$PIPELINE_DIR/../../.." && pwd)" +FN_DIR="$REPO_ROOT/bash/functions/infra" + +# shellcheck source=/dev/null +source "$FN_DIR/systemd_local_install_unit.sh" +# shellcheck source=/dev/null +source "$FN_DIR/systemd_local_enable.sh" +# shellcheck source=/dev/null +source "$FN_DIR/systemd_local_start.sh" +# shellcheck source=/dev/null +source "$FN_DIR/systemd_local_status.sh" + +usage() { + cat <<'USAGE' >&2 +install_systemd_service — registra una app como servicio systemd del sistema. + +Uso: + install_systemd_service --name --exec [opciones] + +Obligatorios: + --name Nombre del servicio (sin .service) + --exec Ruta absoluta al binario/script ExecStart + +Opcionales: + --workdir WorkingDirectory (default: dirname de --exec) + --user User del servicio (default: usuario actual) + --description Description del unit (default: " service") + --env KEY=VAL Variable de entorno (repetible) + --after After= (default: network.target) + --restart Restart= (default: on-failure) + --type Type= (default: simple) + +Salida: JSON consolidado con los resultados de install_unit, enable, start y status. +USAGE + exit 1 +} + +install_systemd_service() { + local name="" exec_path="" workdir="" user="" description="" + local after="network.target" restart="on-failure" type="simple" + local -a envs=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --name) name="$2"; shift 2 ;; + --exec) exec_path="$2"; shift 2 ;; + --workdir) workdir="$2"; shift 2 ;; + --user) user="$2"; shift 2 ;; + --description) description="$2"; shift 2 ;; + --env) envs+=("$2"); shift 2 ;; + --after) after="$2"; shift 2 ;; + --restart) restart="$2"; shift 2 ;; + --type) type="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "opción desconocida: $1" >&2; usage ;; + esac + done + + if [[ -z "$name" || -z "$exec_path" ]]; then + echo "install_systemd_service: faltan --name y --exec" >&2 + usage + fi + + [[ -z "$user" ]] && user="$(id -un)" + [[ -z "$workdir" ]] && workdir="$(dirname "$exec_path")" + [[ -z "$description" ]] && description="$name service" + + # Construir bloque Environment= (uno por línea, orden determinista) + local env_block="" + local e + for e in "${envs[@]}"; do + env_block+="Environment=\"$e\" +" + done + + # Generar unit content (heredoc determinista) + local unit_content + unit_content="[Unit] +Description=$description +After=$after + +[Service] +Type=$type +User=$user +WorkingDirectory=$workdir +${env_block}ExecStart=$exec_path +Restart=$restart +RestartSec=3 + +[Install] +WantedBy=multi-user.target +" + + echo "[install_systemd_service] instalando unit $name..." >&2 + local install_json enable_json start_json status_json + install_json=$(systemd_local_install_unit "$name" "$unit_content") + + echo "[install_systemd_service] enable..." >&2 + enable_json=$(systemd_local_enable "$name") + + echo "[install_systemd_service] start..." >&2 + start_json=$(systemd_local_start "$name") + + # Darle un instante a systemd para estabilizar el estado + sleep 1 + + echo "[install_systemd_service] status..." >&2 + status_json=$(systemd_local_status "$name" 15) + + # JSON consolidado + python3 - "$install_json" "$enable_json" "$start_json" "$status_json" <<'PY' +import json, sys +keys = ["install", "enable", "start", "status"] +out = {k: json.loads(v) for k, v in zip(keys, sys.argv[1:])} +print(json.dumps(out, indent=2)) +PY +} + +# Ejecución directa +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + install_systemd_service "$@" +fi