# Pandora Plugin Executor

# Introducción

`pandora-plugin_exec` es un único binario en Go que sustituye a los plugins por tecnología. Una **plantilla** YAML describe el trabajo a realizar:

1. **Tareas** a ejecutar — peticiones HTTP o ejecuciones de comandos locales.
2. **Filtros** (jq o regexp) que extraen valores de la salida de una tarea hacia **variables**.
3. **Agentes** y **módulos** construidos a partir de esas variables (incluyendo expresiones aritméticas).
4. Un método de **transferencia** que entrega los datos resultantes a Pandora FMS.

El ejecutor ejecuta la plantilla una vez y termina (one-shot). Añadir soporte para una nueva tecnología significa escribir una nueva plantilla YAML — no un plugin nuevo, ni un lenguaje nuevo, ni un pipeline de empaquetado nuevo. Está pensado para usarse junto con el agente de Pandora FMS.

```
template.yml ─▶ ejecutar tareas ─▶ filtrar salidas en variables ─▶ renderizar módulos ─▶ entregar

```

- **Versión**: `0.1.0` (`pandora-plugin_exec --version`).

Audiencia: personas que escriben plantillas y agentes de IA que las generan. Todo lo necesario para escribir una plantilla válida está en este archivo.

De serie, el plugin incluye plantillas ya construidas bajo `templates/`: ejemplos de aprendizaje y sustitutos directos de plugins de agente que antes venían con el agente. Consulta [Plantillas incluidas](#plantillas-incluidas).

# Matriz de compatibilidad

Por ahora solo se ha probado Linux. Windows está planificado, pero todavía no se ha probado.

<table id="bkmrk-plataforma-%2F-runtime"><thead><tr><th>Plataforma / runtime</th><th>Estado</th></tr></thead><tbody><tr><td>Linux amd64</td><td>Probado</td></tr><tr><td>Linux arm64</td><td>No establecido</td></tr><tr><td>Windows amd64</td><td>No establecido</td></tr><tr><td>macOS</td><td>No establecido</td></tr></tbody></table>

El binario entregado es estático, por lo que no tiene dependencias de runtime en el host.

# Prerrequisitos

**Para ejecutar el binario:**

- No se requiere runtime de Go, ni `jq` externo, ni Perl/Python — el binario es estático e incorpora el motor jq (`gojq`).
- Se usa `/bin/sh` para ejecutar los comandos de las tareas `local`, así que los comandos referenciados por la plantilla (`df`, `ss`, `awk`, archivos de `/proc`, etc.) deben existir en el host.

**Por modo de transferencia:**

<table id="bkmrk-modo-requisito-agent"><thead><tr><th>Modo</th><th>Requisito</th></tr></thead><tbody><tr><td>`agent_plugin`</td><td>Un agente de software de Pandora FMS que pueda invocar el binario mediante `module_plugin`</td></tr><tr><td>`tentacle`</td><td>El binario `tentacle_client` (en `$PATH`, o su ruta configurada mediante `transfer.tentacle.binary`) y acceso de red al servidor de Pandora FMS (puerto por defecto `41121`)</td></tr><tr><td>`local`</td><td>Permiso de escritura sobre el directorio de destino (`transfer.local.directory`)</td></tr></tbody></table>

# Parámetros

El ejecutor tiene tres superficies de configuración:

1. **Flags de CLI** (esta sección).
2. **Parámetros en tiempo de ejecución** — `args` posicionales y `params` con nombre, pasados después de los flags (esta sección).
3. **La plantilla YAML** — tareas, agentes/módulos y transferencia. Referencia completa en [Referencia de plantillas](#referencia-de-plantillas).

## Flags de CLI

```
pandora-plugin_exec -t <plantilla.yml> [flags] [args...] [clave=valor...]

```

<table id="bkmrk-flag-efecto--t%2C---te"><thead><tr><th>Flag</th><th>Efecto</th></tr></thead><tbody><tr><td>`-t`, `--template <ruta>`</td><td>Plantilla a ejecutar (obligatorio)</td></tr><tr><td>`--dry-run`</td><td>Imprime el XML generado por stdout, no transfiere</td></tr><tr><td>`--strict`</td><td>Aborta en el primer fallo de tarea en lugar de omitir</td></tr><tr><td>`-v`, `--verbose`</td><td>Registro de depuración en stderr (resultados de tareas, omisiones, avisos)</td></tr><tr><td>`--version`</td><td>Imprime la versión y sale</td></tr></tbody></table>

<table id="bkmrk-c%C3%B3digo-de-salida-sig"><thead><tr><th>Código de salida</th><th>Significado</th></tr></thead><tbody><tr><td>0</td><td>Éxito</td></tr><tr><td>1</td><td>Error de parseo/validación de la plantilla</td></tr><tr><td>2</td><td>Fallo de tarea bajo `--strict`</td></tr><tr><td>3</td><td>Error de transferencia</td></tr></tbody></table>

**Los flags van antes de los argumentos:** el parseo de flags se detiene en el primer token que no es flag, así que los flags deben preceder a los argumentos (`pandora-plugin_exec -t tpl.yml --dry-run sda1`, no `... sda1 --dry-run`). Un argumento que empieza por guion se rechaza con un error claro en lugar de tratarse silenciosamente como posicional.

## Parámetros en tiempo de ejecución

Los tokens que quedan después de los flags alimentan dos variables reservadas, sembradas en el almacén antes de que se ejecute ninguna tarea:

- `args` — lista ordenada de tokens posicionales: `{{ args[0] }}`, `{{ len(args) }}`, `for_each: args`.
- `params` — mapa de tokens con nombre: `{{ params.host }}`, `{{ params.threshold + 5 }}`.

Reglas de parseo:

1. Cada token se divide por el **primer `=` sin escapar**. Un `=` inmediatamente precedido por `\` es un literal escapado y no divide.
2. El token tiene **nombre** solo si la clave candidata (tras desescapar `\=` → `=`) coincide con `[A-Za-z_][A-Za-z0-9_]*`. En caso contrario, todo el token es **posicional**.

Todo token se clasifica — no hay casos de error. Claves con nombre duplicadas: gana la última. Los valores con aspecto numérico se convierten en números, así que la aritmética funciona con ellos. Las variables de tarea no pueden llamarse `args` ni `params` (reservadas); `for_each: args` sí es válido.

**Trampa del escapado en shell:** bash consume `\=` antes de que el ejecutor lo vea. Protege la barra invertida con comillas simples (`'query\=a=b'`) o duplícala (`query\\=a=b`).

<table id="bkmrk-token-se-convierte-e"><thead><tr><th>Token</th><th>Se convierte en</th></tr></thead><tbody><tr><td>`sda1`</td><td>`args[0]` = `"sda1"`</td></tr><tr><td>`90`</td><td>`args[0]` = `90` (número)</td></tr><tr><td>`host=db01`</td><td>`params.host` = `"db01"`</td></tr><tr><td>`threshold=90`</td><td>`params.threshold` = `90` (número)</td></tr><tr><td>`conn=user=a;pw=b`</td><td>`params.conn` = `"user=a;pw=b"` (solo el primer `=` divide)</td></tr><tr><td>`'query\=a=b'`</td><td>posicional `"query=a=b"` (la clave contendría `=` — no es un identificador)</td></tr><tr><td>`=foo`</td><td>posicional `"=foo"` (clave vacía)</td></tr><tr><td>`k=1 k=2`</td><td>`params.k` = `2` (gana la última)</td></tr></tbody></table>

### Parámetros opcionales

Los parámetros en tiempo de ejecución fallan de forma explícita, no silenciosa: referenciar una variable no definida **no** renderiza una cadena vacía — el módulo se omite con un aviso, para proteger contra erratas. Por tanto, un `{{ params.x }}` a secas omite el módulo cuando `x` no se pasó. Para hacer opcional un parámetro, elige un patrón explícito:

<table id="bkmrk-patr%C3%B3n-efecto-%7B%7B-par"><thead><tr><th>Patrón</th><th>Efecto</th></tr></thead><tbody><tr><td>`{{ params.missing ?? "default" }}`</td><td>Renderiza un valor por defecto cuando falta la clave</td></tr><tr><td>`when: "'k' in params"`</td><td>Incluye el módulo solo cuando se pasó la clave</td></tr><tr><td>`{{ len(args) > 1 ? args[1] : "all" }}`</td><td>Protege un acceso posicional según el tamaño</td></tr></tbody></table>

### Parámetros aceptados por las plantillas incluidas

<table id="bkmrk-plantilla-par%C3%A1metro-"><thead><tr><th>Plantilla</th><th>Parámetro</th><th>Por defecto</th><th>Efecto</th></tr></thead><tbody><tr><td>`templates/df_used.yml`</td><td>`include`</td><td>—</td><td>Fuerza monitorizar un montaje cuyo fstype está fuera de la lista por defecto (coincidencias exactas de montaje separadas por comas)</td></tr><tr><td>`templates/df_used.yml`</td><td>`exclude`</td><td>—</td><td>Nunca monitoriza un montaje — **exclude siempre gana sobre include**</td></tr><tr><td>`templates/pandora_netusage.yml`</td><td>`include`</td><td>—</td><td>Monitoriza solo estas interfaces (nombres exactos separados por comas)</td></tr><tr><td>`templates/pandora_netusage.yml`</td><td>`exclude`</td><td>—</td><td>Nunca monitoriza una interfaz — **exclude siempre gana sobre include**</td></tr><tr><td>`templates/pandora_mem_used.yml`</td><td>`mem_critical`</td><td>`95`</td><td>Umbral crítico para el porcentaje de `Memory_Used`</td></tr><tr><td>`templates/pandora_mem_used.yml`</td><td>`swap_critical`</td><td>`95`</td><td>Umbral crítico para el porcentaje de `Swap_Used`</td></tr></tbody></table>

# Ejecución manual

## Formato de ejecución

```
pandora-plugin_exec -t <plantilla.yml> [flags] [args...] [clave=valor...]

```

## Ejemplos

Ejecuta una plantilla incluida sin enviar nada (el binario `pandora-plugin_exec` se entrega ya compilado):

```bash
pandora-plugin_exec -t templates/df_used.yml --dry-run

```

Deberías ver bloques XML `<module>` en stdout, un par por sistema de archivos montado.

Ejecuciones reales con parámetros en tiempo de ejecución:

```bash
pandora-plugin_exec -t templates/df_used.yml                        # lista de fstype por defecto
pandora-plugin_exec -t templates/df_used.yml exclude=/DB,/snap      # descarta dos montajes
pandora-plugin_exec -t templates/df_used.yml include=/mnt/zfs       # monitoriza también un montaje zfs

pandora-plugin_exec -t templates/pandora_netusage.yml                          # todas las interfaces
pandora-plugin_exec -t templates/pandora_netusage.yml exclude=lo,docker0       # descarta loopback y docker0
pandora-plugin_exec -t templates/pandora_netusage.yml include=eth0,ens33       # solo eth0 + ens33
pandora-plugin_exec -t templates/pandora_netusage.yml include=lo exclude=lo    # exclude gana → 0 bytes

pandora-plugin_exec -t templates/pandora_mem_used.yml                          # valores por defecto
pandora-plugin_exec -t templates/pandora_mem_used.yml mem_critical=90 swap_critical=85

```

## Modo verbose

`-v` / `--verbose` escribe el registro de depuración en **stderr**, dejando stdout limpio para el XML generado. Informa de los resultados por tarea (variables almacenadas), módulos omitidos y avisos:

```bash
pandora-plugin_exec -t templates/df_used.yml --dry-run -v

```

No hay archivo de log ni nivel de log; la salida verbose es un único flujo de stderr.

# Configuración en Pandora FMS

El ejecutor es **one-shot**: ejecuta la plantilla una vez y termina. La planificación es externa.

<table id="bkmrk-escenario-c%C3%B3mo-como-"><thead><tr><th>Escenario</th><th>Cómo</th></tr></thead><tbody><tr><td>Como plugin de agente</td><td>La plantilla usa `transfer.mode: agent_plugin`; configuración del agente: `module_plugin /usr/bin/pandora-plugin_exec -t /etc/pandora/templates/df_used.yml`</td></tr><tr><td>Desde cron / lado servidor</td><td>La plantilla usa el modo `tentacle` o `local`; crontab: `*/5 * * * * pandora-plugin_exec -t /etc/pandora/templates/myapp.yml`</td></tr><tr><td>Ejecutándose en el propio servidor de Pandora FMS</td><td>Modo `local` apuntando al directorio de datos entrantes (`/var/spool/pandora/data_in`)</td></tr><tr><td>Depurando una plantilla</td><td>`pandora-plugin_exec -t plantilla.yml --dry-run -v`</td></tr></tbody></table>

No hay asistente ni paquete `.disco` para este plugin: la instalación consiste en colocar el binario (y las plantillas) donde el agente/cron pueda alcanzarlos, y luego conectar la línea `module_plugin` o la entrada de crontab mostradas arriba.

# Agentes y módulos generados por el plugin

**Agentes.** Cada bloque `agents:` se convierte en un documento XML `<agent_data>` (en los modos `tentacle` / `local`). La identidad del agente sale directamente de la plantilla: `name`, `alias`, `parent_agent_name`, `description`, `version`, `os_name`, `os_version`, `timestamp`, `address`, `group`, `interval`, `agent_mode`. En modo `agent_plugin` no se emite cabecera de agente — solo fragmentos `<module>` — y con varios bloques `agents:` cada nombre de módulo se prefija con `<nombre_agente> - <nombre_módulo>` (separador configurable mediante `transfer.module_prefix_separator`) para que los módulos no colisionen bajo el único agente real.

**Módulos.** Un módulo por bloque de módulo resuelto (un bloque `for_each` se resuelve en un módulo por elemento). El tipo de módulo es el que declare la plantilla (`generic_data`, `generic_data_inc`, `generic_data_string`, `async_data`, ...) — el ejecutor reenvía los campos del módulo al servidor de datos sin modificarlos. La lista completa de campos de módulo admitidos está en [Referencia de plantillas](#referencia-de-plantillas).

Una tarea fallida (error de ejecución, estado HTTP fuera de 2xx, filtro con 0 coincidencias) deja su variable sin definir y registra un aviso; un módulo que referencia una variable sin definir se **omite** con un aviso y los módulos restantes se entregan igualmente. `--strict` convierte cualquiera de lo anterior en un aborto (código de salida 2).

---

# Plantillas incluidas

El plugin incluye plantillas ya construidas bajo `templates/`, divididas en dos grupos.

## Ejemplos

<table id="bkmrk-plantilla-prop%C3%B3sito-"><thead><tr><th>Plantilla</th><th>Propósito</th></tr></thead><tbody><tr><td>`templates/http_api_example.yml`</td><td>Encadenamiento de API HTTP con autenticación por token (login → token → petición autenticada)</td></tr><tr><td>`templates/local_commands_example.yml`</td><td>Comandos locales como plugin de agente, mostrando todas las formas de resultado de regexp</td></tr></tbody></table>

## Sustitutos de plugins de agente heredados

<table id="bkmrk-plantilla-sustituye-"><thead><tr><th>Plantilla</th><th>Sustituye a</th></tr></thead><tbody><tr><td>`templates/df_used.yml`</td><td>`pandora_df_used_go` — espacio usado del sistema de archivos</td></tr><tr><td>`templates/pandora_mem_used.yml`</td><td>`pandora_mem_used` — memoria y swap</td></tr><tr><td>`templates/pandora_netusage.yml`</td><td>`pandora_netusage` — uso de red</td></tr></tbody></table>

---

# Referencia de plantillas

Una plantilla es un archivo YAML con tres secciones: `tasks`, `agents` (o `modules` de nivel superior) y `transfer`. El parseo es **estricto**: un nombre de campo desconocido es un error, así que las erratas fallan rápido.

## Ejemplo mínimo completo

```yaml
tasks:
  - name: disk
    type: local
    local: { command: "df --output=pcent / | tail -1 | tr -d ' %'" }
    variable: disk_pct

agents:
  - name: "myhost"
    group: "Servers"
    interval: 300
    modules:
      - name: "Disk used"
        type: generic_data
        data: "{{ disk_pct }}"
        unit: "%"

transfer:
  mode: tentacle
  tentacle: { address: "pandora.example.com" }

```

## `tasks:` — lista ordenada de ejecuciones

Las tareas se ejecutan **secuencialmente, en el orden declarado**. Una tarea puede usar variables producidas por tareas anteriores en cualquiera de sus campos de cadena (encadenamiento: login → token → petición autenticada).

<table id="bkmrk-campo-obligatorio-de"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Descripción</th></tr></thead><tbody><tr><td>`name`</td><td>sí</td><td>Identificador único de tarea (usado en los logs)</td></tr><tr><td>`type`</td><td>sí</td><td>`request` o `local`</td></tr><tr><td>`request`</td><td>si type=request</td><td>Especificación de petición HTTP (abajo)</td></tr><tr><td>`local`</td><td>si type=local</td><td>Especificación de comando (abajo)</td></tr><tr><td>`filter`</td><td>no</td><td>Filtro de extracción (abajo). Sin él, se almacena la salida bruta recortada</td></tr><tr><td>`variable`</td><td>no</td><td>Nombre bajo el que almacenar el resultado. Debe coincidir con `[A-Za-z_][A-Za-z0-9_]*`</td></tr></tbody></table>

### `request:` (HTTP)

<table id="bkmrk-campo-obligatorio-po"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Por defecto</th><th>Descripción</th></tr></thead><tbody><tr><td>`url`</td><td>sí</td><td>—</td><td>URL de destino. Admite `{{ }}`</td></tr><tr><td>`method`</td><td>no</td><td>`GET`</td><td>`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`</td></tr><tr><td>`headers`</td><td>no</td><td>—</td><td>Mapa de cabecera → valor. Los valores admiten `{{ }}`</td></tr><tr><td>`body`</td><td>no</td><td>—</td><td>Cuerpo de la petición. Admite `{{ }}`</td></tr><tr><td>`timeout`</td><td>no</td><td>`30`</td><td>Segundos</td></tr><tr><td>`skip_tls_verify`</td><td>no</td><td>`false`</td><td>Acepta certificados TLS inválidos</td></tr></tbody></table>

Una respuesta con estado fuera de 2xx es un fallo de tarea. Los cuerpos de respuesta están limitados a 10 MiB.

### `local:` (comando)

<table id="bkmrk-campo-obligatorio-po-1"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Por defecto</th><th>Descripción</th></tr></thead><tbody><tr><td>`command`</td><td>sí</td><td>—</td><td>Se ejecuta con `/bin/sh -c`. Admite `{{ }}`. El `$` de shell no se toca (`awk '{print $5}'` funciona tal cual)</td></tr><tr><td>`timeout`</td><td>no</td><td>`30`</td><td>Segundos; el proceso se mata al expirar</td></tr></tbody></table>

Un código de salida distinto de cero es un fallo de tarea (stderr se incluye en el log).

### `filter:` — extracción de valores

<table id="bkmrk-campo-obligatorio-de-1"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Descripción</th></tr></thead><tbody><tr><td>`type`</td><td>sí</td><td>`jq` o `regexp`</td></tr><tr><td>`expression`</td><td>sí</td><td>La expresión del filtro</td></tr></tbody></table>

**Cardinalidad del resultado (ambos tipos de filtro):** 0 coincidencias → variable sin definir (semántica de fallo de tarea) · 1 coincidencia → escalar · N coincidencias → array.

**`jq`** — la salida de la tarea debe ser JSON válido. La expresión se ejecuta con un motor jq incorporado (no se necesita el binario `jq`). Cada valor que emite la expresión es un elemento: `.token` → escalar, `.items[].name` → array, `.items[]` → array de objetos.

**`regexp`** — sintaxis RE2 de Go (sin backtracking, sin lookahead) aplicada a la salida bruta. Por coincidencia:

<table id="bkmrk-forma-del-patr%C3%B3n-cad"><thead><tr><th>Forma del patrón</th><th>Cada coincidencia se convierte en</th></tr></thead><tbody><tr><td>Sin grupo de captura (`cpu\d`)</td><td>La coincidencia completa (cadena)</td></tr><tr><td>Un grupo sin nombre (`(\d+)%`)</td><td>El grupo de captura 1 (cadena)</td></tr><tr><td>2+ grupos sin nombre (`(a)(b)(c)`)</td><td>Array posicional: `[[a, b, c], ...]`</td></tr><tr><td>Grupos con nombre (`(?P<mount>...)`)</td><td>Un objeto: nombre del grupo → texto capturado</td></tr></tbody></table>

Usa `(?m)` para patrones multilínea anclados por línea y `(?i)` para coincidencia sin distinción de mayúsculas. No mezcles grupos con nombre y sin nombre en la misma regexp si quieres un acceso predecible; la forma con nombre solo expone los campos con nombre.

**Coerción de tipos:** las cadenas extraídas con aspecto numérico se convierten en números (recursivamente, incluyendo campos de objetos), así que la aritmética funciona con ellas.

## Variables y expresiones `{{ }}`

Cualquier campo de cadena de tareas (después de la tarea que la produce), agentes y módulos puede incluir `{{ expresión }}`. Las expresiones se evalúan con [expr-lang](https://expr-lang.org) contra el almacén de variables.

<table id="bkmrk-expresi%C3%B3n-resultado-"><thead><tr><th>Expresión</th><th>Resultado</th></tr></thead><tbody><tr><td>`{{ token }}`</td><td>Valor de la variable</td></tr><tr><td>`{{ 100 - disk_pct }}`</td><td>Aritmética</td></tr><tr><td>`{{ names[0] }}`</td><td>Indexado de array</td></tr><tr><td>`{{ value.mount }}`</td><td>Campo de objeto (dentro de `for_each`)</td></tr><tr><td>`{{ names[index] }}`</td><td>Array paralelo sincronizado (dentro de `for_each`)</td></tr></tbody></table>

Referenciar una variable no definida es un error → el módulo afectado se omite (o la ejecución aborta bajo `--strict`).

## `agents:` — un bloque por agente

Obligatorio a menos que uses `modules:` de nivel superior (ver el modo `agent_plugin`). Cada agente se convierte en un documento XML `<agent_data>`.

<table id="bkmrk-campo-obligatorio-de-2"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Descripción</th></tr></thead><tbody><tr><td>`name`</td><td>sí</td><td>Nombre del agente</td></tr><tr><td>`modules`</td><td>sí</td><td>Lista de bloques de módulo (al menos uno)</td></tr><tr><td>`alias`</td><td>no</td><td>Alias del agente</td></tr><tr><td>`parent_agent_name`</td><td>no</td><td>Agente padre</td></tr><tr><td>`description`</td><td>no</td><td>Descripción</td></tr><tr><td>`version`</td><td>no</td><td>Cadena de versión del agente</td></tr><tr><td>`os_name`, `os_version`</td><td>no</td><td>Identificación del SO</td></tr><tr><td>`timestamp`</td><td>no</td><td>Sobrescribe la marca de tiempo de los datos</td></tr><tr><td>`address`</td><td>no</td><td>IP/nombre de host</td></tr><tr><td>`group`</td><td>no</td><td>Grupo de destino</td></tr><tr><td>`interval`</td><td>no</td><td>Segundos (entero)</td></tr><tr><td>`agent_mode`</td><td>no</td><td>Modo del agente</td></tr></tbody></table>

Todos los campos de cadena admiten `{{ }}`.

## `modules:` — bloques de módulo

Los bloques de módulo viven dentro de un agente, o a nivel superior (solo con `transfer.mode: agent_plugin`).

Campos de control (semántica del ejecutor, no se envían a Pandora):

<table id="bkmrk-campo-descripci%C3%B3n-fo"><thead><tr><th>Campo</th><th>Descripción</th></tr></thead><tbody><tr><td>`for_each`</td><td>Nombre de variable array. El bloque se expande a **un módulo por elemento**; `{{ value }}` e `{{ index }}` quedan disponibles. Una variable escalar itera como array de 1 elemento. Para grupos de captura sin nombre con 2+ grupos, cada `value` es un array, así que puedes usar `{{ value[0] }}`. ¿Varios módulos por elemento? Escribe varios bloques con el mismo `for_each`</td></tr><tr><td>`when`</td><td>Expresión booleana expr-lang en bruto (sin `{{ }}`). Falso → módulo (o elemento) omitido. Dentro de `for_each` se evalúa por elemento y ve `value`/`index`. Ejemplo: `when: 'not (value.mount matches "^/DB")'`</td></tr></tbody></table>

Campos de datos — los dos obligatorios más el conjunto completo aceptado por el servidor de datos. Los valores pueden ser números o cadenas en YAML; todos admiten `{{ }}`:

<table id="bkmrk-campo-obligatorio-de-3"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Descripción</th></tr></thead><tbody><tr><td>`name`</td><td>sí</td><td>Nombre del módulo</td></tr><tr><td>`type`</td><td>sí</td><td>Tipo de módulo (`generic_data`, `generic_proc`, `generic_data_string`, `async_data`, ...)</td></tr><tr><td>`data`</td><td>no</td><td>Valor del módulo</td></tr><tr><td>`description`</td><td>no</td><td>Descripción</td></tr><tr><td>`unit`</td><td>no</td><td>Etiqueta de unidad</td></tr><tr><td>`interval`</td><td>no</td><td>Intervalo del módulo</td></tr><tr><td>`tags`</td><td>no</td><td>Etiquetas</td></tr><tr><td>`module_group`</td><td>no</td><td>Grupo de módulo</td></tr><tr><td>`module_parent`, `module_parent_unlink`</td><td>no</td><td>Relación de módulo padre</td></tr><tr><td>`min_warning`, `max_warning`, `min_critical`, `max_critical`</td><td>no</td><td>Umbrales numéricos</td></tr><tr><td>`min_warning_forced`, `max_warning_forced`, `min_critical_forced`, `max_critical_forced`</td><td>no</td><td>Variantes de umbral forzado</td></tr><tr><td>`str_warning`, `str_critical`</td><td>no</td><td>Umbrales por coincidencia de cadena</td></tr><tr><td>`str_warning_forced`, `str_critical_forced`</td><td>no</td><td>Umbrales de cadena forzados</td></tr><tr><td>`warning_inverse`, `critical_inverse`</td><td>no</td><td>Invertir la lógica del umbral</td></tr><tr><td>`min`, `max`</td><td>no</td><td>Rango de datos válido</td></tr><tr><td>`post_process`</td><td>no</td><td>Multiplicador aplicado por el servidor</td></tr><tr><td>`disabled`</td><td>no</td><td>Crear deshabilitado</td></tr><tr><td>`status`</td><td>no</td><td>Forzar estado</td></tr><tr><td>`timestamp`</td><td>no</td><td>Sobrescribe la marca de tiempo de los datos</td></tr><tr><td>`custom_id`</td><td>no</td><td>Identificador personalizado</td></tr><tr><td>`critical_instructions`, `warning_instructions`, `unknown_instructions`</td><td>no</td><td>Instrucciones del operador</td></tr><tr><td>`quiet`</td><td>no</td><td>Modo silencioso</td></tr><tr><td>`min_ff_event`, `min_ff_event_normal`, `min_ff_event_warning`, `min_ff_event_critical`</td><td>no</td><td>Umbrales FlipFlop</td></tr><tr><td>`module_ff_interval`, `ff_type`, `ff_timeout`, `each_ff`</td><td>no</td><td>Comportamiento FlipFlop</td></tr><tr><td>`crontab`</td><td>no</td><td>Planificación de módulo estilo cron</td></tr><tr><td>`extra_data`</td><td>no</td><td>Carga extra</td></tr><tr><td>`alert_templates`</td><td>no</td><td>Lista de nombres de plantillas de alerta a vincular</td></tr></tbody></table>

## `transfer:` — entrega

<table id="bkmrk-campo-obligatorio-de-4"><thead><tr><th>Campo</th><th>Obligatorio</th><th>Descripción</th></tr></thead><tbody><tr><td>`mode`</td><td>sí</td><td>`tentacle`, `local` o `agent_plugin`</td></tr><tr><td>`tentacle.address`</td><td>para tentacle</td><td>Dirección del servidor Pandora</td></tr><tr><td>`tentacle.port`</td><td>no (por defecto `41121`)</td><td>Puerto tentacle</td></tr><tr><td>`tentacle.binary`</td><td>no</td><td>Ruta a `tentacle_client` si no está en `$PATH`</td></tr><tr><td>`tentacle.extra_args`</td><td>no</td><td>Argumentos extra del cliente tentacle (lista)</td></tr><tr><td>`local.directory`</td><td>para local</td><td>Directorio donde se escribe el archivo XML `.data`</td></tr><tr><td>`module_prefix_separator`</td><td>no (por defecto `" - "`)</td><td>Separador para el prefijado multi-agente en modo `agent_plugin`</td></tr></tbody></table>

<table id="bkmrk-modo-salida-caso-de-"><thead><tr><th>Modo</th><th>Salida</th><th>Caso de uso</th></tr></thead><tbody><tr><td>`tentacle`</td><td>XML `<agent_data>` completo enviado vía cliente tentacle</td><td>Ejecución remota (cron, Discovery)</td></tr><tr><td>`local`</td><td>XML `<agent_data>` completo escrito en un directorio</td><td>Ejecutarse en el propio servidor Pandora (apuntando al directorio de entrada), o depuración</td></tr><tr><td>`agent_plugin`</td><td>Solo fragmentos `<module>` impresos en stdout — el agente real añade la cabecera</td><td>Ejecutarse como `module_plugin` de un agente de software</td></tr></tbody></table>

Particularidades de `agent_plugin`:

- Se permite `modules:` de nivel superior sin ningún bloque `agents:` (y solo se permite en este modo).
- Con **varios** agentes definidos, cada nombre de módulo se prefija con `<nombre_agente> - <nombre_módulo>` para que los módulos no colisionen bajo el único agente real.

---

# Crear tus propias plantillas

Una plantilla es solo YAML. Constrúyela de arriba a abajo: decide **qué medir** (tareas), **cómo extraer los números** (filtros), **qué módulos exponer** (agentes/módulos) y **cómo entregar** (transferencia).

## Paso 1 — Parte de un ejemplo incluido

`templates/local_commands_example.yml` es la plantilla completa más corta y demuestra las tres formas de resultado de regexp. Cópiala y sustituye las tareas:

```bash
cp templates/local_commands_example.yml templates/my_template.yml

```

## Paso 2 — Escribe las tareas

Cada tarea ejecuta un comando o una petición HTTP y almacena su salida (opcionalmente filtrada) en una variable.

```yaml
tasks:
  # Un escalar: un valor, sin filtro (se almacena la salida bruta recortada).
  - name: uptime_seconds
    type: local
    local: { command: "awk '{print int($1)}' /proc/uptime" }
    variable: uptime

  # Un escalar mediante una regexp con una sola captura.
  - name: load_avg
    type: local
    local: { command: "cat /proc/loadavg" }
    filter:
      type: regexp
      expression: '^(\d+\.\d+)'
    variable: load1

  # Un array de objetos mediante grupos de captura con nombre.
  - name: df
    type: local
    local: { command: "df -kTP" }
    filter:
      type: regexp
      expression: '(?m)^(?P<fs>\S+)\s+(?P<fstype>\S+)\s+(?P<total>\d+)\s+(?P<used>\d+)\s+\d+\s+(?P<pct>\d+)%\s+(?P<mount>.+)$'
    variable: filesystems

```

## Paso 3 — Convierte variables en módulos

Una variable escalar alimenta directamente un módulo; un array alimenta bloques `for_each` (un módulo por elemento).

```yaml
modules:
  - name: "Uptime seconds"
    type: generic_data
    data: "{{ uptime }}"

  - name: "Load average 1m"
    type: generic_data
    data: "{{ load1 }}"

  # Un módulo por sistema de archivos; value.* lee los grupos de captura con nombre.
  - for_each: filesystems
    when: 'value.fstype != "tmpfs"'
    name: "DiskUsed_{{ value.mount }}"
    type: generic_data
    data: "{{ value.pct }}"
    unit: "%"

```

Dentro de un bloque `for_each`:

- `{{ value }}` es el elemento actual, `{{ index }}` su posición (base 0).
- Los grupos con nombre hacen que cada elemento sea un **objeto** → accede a los campos como `{{ value.mount }}`.
- 2+ grupos sin nombre hacen que cada elemento sea un **array posicional** → accede como `{{ value[0] }}`, `{{ value[1] }}`, ...; escribe varios bloques con el mismo `for_each` para emitir varios módulos por elemento.

## Paso 4 — Elige el modo de transferencia

```yaml
transfer:
  mode: agent_plugin            # salida solo de módulos; ejecutar como plugin de agente de software

```

```yaml
transfer:
  mode: tentacle                # XML de agente completo a un servidor Pandora
  tentacle: { address: "pandora.example.com" }

```

```yaml
transfer:
  mode: local                   # XML de agente completo escrito en un directorio
  local: { directory: "/var/spool/pandora/data_in" }

```

## Paso 5 — Valida antes de entregar

```bash
pandora-plugin_exec -t templates/my_template.yml --dry-run -v

```

`--dry-run` imprime el XML generado sin transferir; `-v` muestra qué variables se almacenaron y qué módulos se omitieron. El parser es estricto, así que cualquier errata en un nombre de campo falla inmediatamente.

## Errores comunes

- **Los flags después de los argumentos no funcionan.** El parseo de flags se detiene en el primer token que no es flag: pon `--dry-run` antes de cualquier argumento posicional.
- **Las variables no definidas omiten el módulo**, no renderizan vacío. Usa `{{ params.x ?? "default" }}` o `when: "'x' in params"` para parámetros opcionales.
- **`filter.expression` no es expr.** Es jq o una regexp RE2 según `filter.type`. expr solo se usa dentro de `{{ }}` y en `when:`.
- **Duplica la barra invertida en literales regexp dentro de `when:`** — los literales de cadena de expr procesan escapes, así que `\\d` entrega `\d` al motor de regexp.
- **RE2 no tiene lookahead/backtracking** — usa `when:` para exclusiones (p. ej. `when: 'not (value.mount matches "^/DB")'`).

---

# Referencia rápida del lenguaje de expresiones

Los placeholders `{{ }}` y las condiciones `when:` son expresiones [expr-lang](https://expr-lang.org/docs/language-definition). Funciones útiles para autores de plantillas:

<table id="bkmrk-categor%C3%ADa-funciones-"><thead><tr><th>Categoría</th><th>Funciones / operadores</th></tr></thead><tbody><tr><td>Cadenas</td><td>`split(s, sep)` · `join(list, sep)` · `upper(s)` · `lower(s)` · `trim(s)` · `replace(s, old, new)` · `contains(s, sub)` · `startsWith(s, pre)` · `endsWith(s, suf)` · `s matches "patrón-re2"`</td></tr><tr><td>Colecciones</td><td>`x in list` · `len(list)` · `list[i]` · `first(list)` · `last(list)` · `filter(list, pred)` · `map(list, expr)` · `all`/`any`/`none(list, pred)` · `sort(list)` · `uniq(list)` · `concat(a, b)`</td></tr><tr><td>Lógica y nil</td><td>`a ?? "default"` (coalescencia de nil) · `obj?.field` (encadenamiento opcional) · `cond ? a : b` (ternario) · `'clave' in map` (presencia de clave)</td></tr><tr><td>Números</td><td>`+ - * / %` · `abs(x)` · `min`/`max(a, b)` · `sum(list)` · `avg(list)` · `int(x)` · `float(x)` · `string(x)`</td></tr><tr><td>JSON</td><td>`toJSON(x)` · `fromJSON(s)`</td></tr></tbody></table>

En los predicados de `filter`/`map`/`all`/`any`/`none` el elemento actual es `#` (p. ej. `filter(filesystems, #.pct > 90)`).

**No confundas lenguajes:** el `filter.expression` de la tarea **no** es expr — es jq (vía gojq) o una regexp RE2, según `filter.type`. expr solo se usa dentro de `{{ }}` y en `when:`.

---

# Ejemplo práctico: sustituir un plugin real

`templates/df_used.yml` sustituye al plugin de agente `pandora_df_used_go`. El patrón a aprender: **una ejecución → array de objetos → N módulos**.

```yaml
tasks:
  - name: df
    type: local
    local: { command: "df -kTP" }
    filter:
      type: regexp
      # Grupos con nombre: cada fila de df se convierte en {fs, fstype, total, used, pct, mount}
      expression: '(?m)^(?P<fs>\S+)\s+(?P<fstype>\S+)\s+(?P<total>\d+)\s+(?P<used>\d+)\s+\d+\s+(?P<pct>\d+)%\s+(?P<mount>.+)$'
    variable: filesystems

modules:
  - for_each: filesystems
    when: >
      (value.fstype matches "^(?i:adfs|affs|autofs|btrfs|cifs|coda|coherent|efs|ext\\d?|hfs|hfsplus|hpfs|jfs|minix|msdos|ncpfs|nfs4?|ntfs|proc|qnx4|reiserfs|smbfs|sysv|ubifs|udf|ufs|umsdos|usbfs|vfat|xenix|xfs|xiafs)$"
      || value.mount in split(params.include ?? "", ","))
      && not (value.mount in split(params.exclude ?? "", ","))
    name: "DiskUsed_{{ value.mount }}"
    type: generic_data
    data: "{{ value.pct }}"
    unit: "%"

  - for_each: filesystems
    when: >
      (value.fstype matches "^(?i:adfs|affs|autofs|btrfs|cifs|coda|coherent|efs|ext\\d?|hfs|hfsplus|hpfs|jfs|minix|msdos|ncpfs|nfs4?|ntfs|proc|qnx4|reiserfs|smbfs|sysv|ubifs|udf|ufs|umsdos|usbfs|vfat|xenix|xfs|xiafs)$"
      || value.mount in split(params.include ?? "", ","))
      && not (value.mount in split(params.exclude ?? "", ","))
    name: "DiskUsed_{{ value.mount }} bytes"
    type: generic_data
    data: "{{ value.used }}"
    unit: "bytes"

transfer:
  mode: agent_plugin

```

La regexp captura **todas** las filas de df; la lista permitida de fstype por defecto vive en `when:` para que los parámetros en tiempo de ejecución puedan sobrescribirla. Observa el escalar plegado (`when: >`) y la barra invertida duplicada (`\\d`): los literales de cadena de expr procesan escapes, así que `\\d` entrega un `\d` literal al motor de regexp.

## Ejemplo de capturas posicionales

Úsalo cuando quieras el orden de captura en lugar de nombres de campo:

```yaml
tasks:
  - name: fs
    type: local
    local:
      command: |
        printf '/dev/sda1 ext4 40 81 /\n/dev/sdb1 xfs 90 12 /data\n'
    filter:
      type: regexp
      expression: '(?m)^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(.+)$'
    variable: filesystems

modules:
  - for_each: filesystems
    name: "FS {{ value[0] }}"
    type: generic_data
    data: "{{ value[3] }}"
    unit: "%"

transfer:
  mode: agent_plugin

```

## Ejemplo de API HTTP

`templates/http_api_example.yml` demuestra el encadenamiento con autenticación por token: una petición `login` almacena el token de sesión, y las peticiones posteriores lo incorporan mediante `Authorization: "Bearer {{ token }}"`.

---

# Lista de verificación de plantillas (para humanos y generadores de IA)

- [ ]  Cada tarea tiene un `name` único y su bloque coincide con su `type` (`request:` xor `local:`).
- [ ]  Cada nombre de `variable` coincide con `[A-Za-z_][A-Za-z0-9_]*` (y no es `args`/`params`).
- [ ]  Las tareas están ordenadas de modo que las variables se producen antes de consumirse.
- [ ]  O bien `agents:` (con ≥1 módulo cada uno) o `modules:` de nivel superior + modo `agent_plugin` — nunca ambos.
- [ ]  Cada módulo tiene `name` y `type`.
- [ ]  `for_each` referencia una variable que un filtro produce como array; los campos de elemento se acceden como `{{ value.field }}` para capturas con nombre o `{{ value[0] }}` para capturas posicionales.
- [ ]  `when:` es una expresión en bruto (sin `{{ }}`); todo lo demás usa placeholders `{{ }}`.
- [ ]  `transfer.mode` tiene sus opciones obligatorias (`tentacle.address` / `local.directory`).
- [ ]  Validado con `pandora-plugin_exec -t plantilla.yml --dry-run -v` antes de entregar.