Pandora Plugin Executor

Generic Plugin Executor for Pandora FMS.

Introduction

pandora-plugin_exec is a single Go binary that replaces per-technology plugins. A YAML template describes the work to do:

  1. Tasks to run — HTTP requests or local command executions.
  2. Filters (jq or regexp) that extract values from a task's output into variables.
  3. Agents and modules built from those variables (including arithmetic expressions).
  4. A transfer method that delivers the resulting data to Pandora FMS.

The executor runs the template once and exits (one-shot). Adding support for a new technology means writing a new YAML template — not a new plugin, not a new language, not a new packaging pipeline. It is meant to be used alongside the Pandora FMS agent.

template.yml ─▶ run tasks ─▶ filter outputs into variables ─▶ render modules ─▶ deliver

Audience: humans writing templates and AI agents generating them. Everything needed to write a valid template is in this file.

Out of the box the plugin ships pre-built templates under templates/: learning examples plus drop-in replacements for agent plugins that used to ship with the agent. See Shipped templates.

Compatibility matrix

Only Linux has been exercised so far. Windows is planned but has not been tested yet.

Platform / runtime Status
Linux amd64 Tested
Linux arm64 Not established
Windows amd64 Not established
macOS Not established

The delivered binary is static, so it has no host runtime dependencies.

Pre requisites

To run the binary:

Per transfer mode:

Mode Requirement
agent_plugin A Pandora FMS software agent that can invoke the binary via module_plugin
tentacle The tentacle_client binary (on $PATH, or its path set via transfer.tentacle.binary) and network access to the Pandora FMS server (default port 41121)
local Write permission on the target directory (transfer.local.directory)

Parameters

The executor has three configuration surfaces:

  1. CLI flags (this section).
  2. Runtime parameters — positional args and named params passed after the flags (this section).
  3. The YAML template — tasks, agents/modules and transfer. Full reference in Template reference.

CLI flags

pandora-plugin_exec -t <template.yml> [flags] [args...] [key=value...]
Flag Effect
-t, --template <path> Template to execute (required)
--dry-run Print the generated XML to stdout, do not transfer
--strict Abort on the first task failure instead of skipping
-v, --verbose Debug logging to stderr (task results, skips, warnings)
--version Print version and exit
Exit code Meaning
0 Success
1 Template parse/validation error
2 Task failure under --strict
3 Transfer error

Flags before arguments: flag parsing stops at the first non-flag token, so flags must precede the arguments (pandora-plugin_exec -t tpl.yml --dry-run sda1, not ... sda1 --dry-run). A dash-leading argument is rejected with a clear error instead of being silently treated as a positional.

Runtime parameters

Tokens left after the flags feed two reserved variables, seeded into the store before any task runs:

Parsing rules:

  1. Each token splits on the first unescaped =. A = immediately preceded by \ is an escaped literal and does not split.
  2. The token is named only if the candidate key (after unescaping \==) matches [A-Za-z_][A-Za-z0-9_]*. Otherwise the whole token is positional.

Every token classifies — there are no error cases. Duplicate named keys: last wins. Numeric-looking values become numbers, so arithmetic works on them. Task variables cannot be named args or params (reserved); for_each: args is fine.

Shell quoting gotcha: bash consumes \= before the executor sees it. Protect the backslash with single quotes ('query\=a=b') or double it (query\\=a=b).

Token Becomes
sda1 args[0] = "sda1"
90 args[0] = 90 (number)
host=db01 params.host = "db01"
threshold=90 params.threshold = 90 (number)
conn=user=a;pw=b params.conn = "user=a;pw=b" (only the first = splits)
'query\=a=b' positional "query=a=b" (the key would contain = — not an identifier)
=foo positional "=foo" (empty key)
k=1 k=2 params.k = 2 (last wins)

Optional parameters

Runtime parameters fail loudly, not silently: referencing an undefined variable does not render an empty string — the module is skipped with a warning, to protect against typos. A bare {{ params.x }} therefore skips the module when x was not passed. To make a parameter optional, pick an explicit pattern:

Pattern Effect
{{ params.missing ?? "default" }} Render a default when the key is absent
when: "'k' in params" Include the module only when the key was passed
{{ len(args) > 1 ? args[1] : "all" }} Guard a positional access by count

Parameters accepted by the shipped templates

Template Parameter Default Effect
templates/df_used.yml include Force-monitor a mount whose fstype is outside the default list (comma-separated exact mount matches)
templates/df_used.yml exclude Never monitor a mount — exclude always wins over include
templates/pandora_netusage.yml include Monitor only these interfaces (comma-separated exact names)
templates/pandora_netusage.yml exclude Never monitor an interface — exclude always wins over include
templates/pandora_mem_used.yml mem_critical 95 Critical threshold for Memory_Used percent
templates/pandora_mem_used.yml swap_critical 95 Critical threshold for Swap_Used percent

Manual execution

Execution format

pandora-plugin_exec -t <template.yml> [flags] [args...] [key=value...]

Examples

Run a shipped template without sending anything (the pandora-plugin_exec binary is delivered pre-built):

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

You should see <module> XML blocks on stdout, one pair per mounted filesystem.

Real runs with runtime parameters:

pandora-plugin_exec -t templates/df_used.yml                        # default fstype allow-list
pandora-plugin_exec -t templates/df_used.yml exclude=/DB,/snap      # drop two mounts
pandora-plugin_exec -t templates/df_used.yml include=/mnt/zfs       # monitor a zfs mount too

pandora-plugin_exec -t templates/pandora_netusage.yml                          # every interface
pandora-plugin_exec -t templates/pandora_netusage.yml exclude=lo,docker0       # drop loopback and docker0
pandora-plugin_exec -t templates/pandora_netusage.yml include=eth0,ens33       # only eth0 + ens33
pandora-plugin_exec -t templates/pandora_netusage.yml include=lo exclude=lo    # exclude wins → 0 bytes

pandora-plugin_exec -t templates/pandora_mem_used.yml                          # defaults
pandora-plugin_exec -t templates/pandora_mem_used.yml mem_critical=90 swap_critical=85

Verbose mode

-v / --verbose writes debug logging to stderr, leaving stdout clean for the generated XML. It reports per-task results (stored variables), skipped modules, and warnings:

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

There is no log file or log level; verbose output is a single stderr stream.

Configuration in PandoraFMS

The executor is one-shot: it runs the template once and exits. Scheduling is external.

Scenario How
As an agent plugin Template uses transfer.mode: agent_plugin; agent config: module_plugin /usr/bin/pandora-plugin_exec -t /etc/pandora/templates/df_used.yml
From cron / server side Template uses tentacle or local mode; crontab: */5 * * * * pandora-plugin_exec -t /etc/pandora/templates/myapp.yml
Running on the Pandora FMS server itself local mode pointed at the incoming data directory (/var/spool/pandora/data_in)
Debugging a template pandora-plugin_exec -t template.yml --dry-run -v

There is no wizard or .disco package for this plugin: installation means placing the binary (and templates) where the agent/cron can reach them, then wiring the module_plugin line or crontab entry shown above.

Agent and modules generated by the plugin

Agents. Each agents: block becomes one <agent_data> XML document (in tentacle / local modes). Agent identity comes straight from the template: name, alias, parent_agent_name, description, version, os_name, os_version, timestamp, address, group, interval, agent_mode. In agent_plugin mode no agent header is emitted — only <module> fragments — and with several agents: blocks each module name is prefixed <agent_name> - <module_name> (separator configurable via transfer.module_prefix_separator) so modules cannot collide under the one real agent.

Modules. One module per resolved module block (a for_each block resolves to one module per element). Module type is whatever the template declares (generic_data, generic_data_inc, generic_data_string, async_data, ...) — the executor forwards the module fields to the dataserver unchanged. The full set of supported module fields is listed in Template reference.

A failed task (execution error, non-2xx HTTP status, filter with 0 matches) leaves its variable unset and logs a warning; a module that references an unset variable is skipped with a warning and the remaining modules are still delivered. --strict turns any of the above into an abort (exit code 2).


Shipped templates

The plugin ships with pre-built templates under templates/, split into two groups.

Examples

Template Purpose
templates/http_api_example.yml Token-auth HTTP API chaining (login → token → authenticated request)
templates/local_commands_example.yml Local commands as an agent plugin, showing all regexp result shapes

Replacements for legacy agent plugins

Template Replaces
templates/df_used.yml pandora_df_used_go — filesystem used space
templates/pandora_mem_used.yml pandora_mem_used — memory and swap
templates/pandora_netusage.yml pandora_netusage — network usage

Template reference

A template is a YAML file with three sections: tasks, agents (or top-level modules), and transfer. Parsing is strict: an unknown field name is an error, so typos fail fast.

Minimal complete example

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: — ordered list of executions

Tasks run sequentially, in declared order. A task may use variables produced by earlier tasks in any of its string fields (chaining: login → token → authenticated request).

Field Required Description
name yes Unique task identifier (used in logs)
type yes request or local
request if type=request HTTP request spec (below)
local if type=local Command spec (below)
filter no Extraction filter (below). Without it, the trimmed raw output is stored
variable no Name to store the result under. Must match [A-Za-z_][A-Za-z0-9_]*

request: (HTTP)

Field Required Default Description
url yes Target URL. Supports {{ }}
method no GET GET, POST, PUT, DELETE, PATCH, HEAD
headers no Map of header → value. Values support {{ }}
body no Request body. Supports {{ }}
timeout no 30 Seconds
skip_tls_verify no false Accept invalid TLS certificates

A response with status outside 2xx is a task failure. Response bodies are capped at 10 MiB.

local: (command)

Field Required Default Description
command yes Executed with /bin/sh -c. Supports {{ }}. Shell $ is untouched (awk '{print $5}' works as-is)
timeout no 30 Seconds; the process is killed on expiry

A non-zero exit code is a task failure (stderr is included in the log).

filter: — extracting values

Field Required Description
type yes jq or regexp
expression yes The filter expression

Result cardinality (both filter types): 0 matches → variable unset (task failure semantics) · 1 match → scalar · N matches → array.

jq — the task output must be valid JSON. The expression runs with an embedded jq engine (no jq binary needed). Each value the expression emits is one element: .token → scalar, .items[].name → array, .items[] → array of objects.

regexp — Go RE2 syntax (no backtracking, no lookahead) applied to the raw output. Per match:

Pattern shape Each match becomes
No capture group (cpu\d) The full match (string)
One unnamed group ((\d+)%) Capture group 1 (string)
2+ unnamed groups ((a)(b)(c)) Positional array: [[a, b, c], ...]
Named groups ((?P<mount>...)) An object: group name → captured text

Use (?m) for line-anchored multi-line patterns and (?i) for case-insensitive matching. Do not mix named and unnamed groups in the same regexp if you want predictable access; the named form exposes only the named fields.

Type coercion: extracted strings that look numeric become numbers (recursively, including object fields), so arithmetic works on them.

Variables and {{ }} expressions

Any string field of tasks (after the producing task), agents, and modules may embed {{ expression }}. Expressions are evaluated with expr-lang against the variable store.

Expression Result
{{ token }} Variable value
{{ 100 - disk_pct }} Arithmetic
{{ names[0] }} Array indexing
{{ value.mount }} Object field (inside for_each)
{{ names[index] }} Lockstep parallel array (inside for_each)

Referencing an undefined variable is an error → the affected module is skipped (or the run aborts under --strict).

agents: — one block per agent

Required unless you use top-level modules: (see agent_plugin mode). Each agent becomes one <agent_data> XML document.

Field Required Description
name yes Agent name
modules yes List of module blocks (at least one)
alias no Agent alias
parent_agent_name no Parent agent
description no Description
version no Agent version string
os_name, os_version no OS identification
timestamp no Override data timestamp
address no IP/hostname
group no Target group
interval no Seconds (integer)
agent_mode no Agent mode

All string fields support {{ }}.

modules: — module blocks

Module blocks live inside an agent, or at the top level (only with transfer.mode: agent_plugin).

Control fields (executor semantics, not sent to Pandora):

Field Description
for_each Array variable name. The block expands to one module per element; {{ value }} and {{ index }} become available. A scalar variable iterates as a 1-element array. For unnamed capture groups with 2+ groups, each value is an array so you can use {{ value[0] }}. Need several modules per element? Write several blocks with the same for_each
when Raw boolean expr-lang expression (no {{ }}). False → module (or element) skipped. Inside for_each it is evaluated per element and sees value/index. Example: when: 'not (value.mount matches "^/DB")'

Data fields — the two required plus the full set accepted by the dataserver. Values may be numbers or strings in YAML; all support {{ }}:

Field Required Description
name yes Module name
type yes Module type (generic_data, generic_proc, generic_data_string, async_data, ...)
data no Module value
description no Description
unit no Unit label
interval no Module interval
tags no Tags
module_group no Module group
module_parent, module_parent_unlink no Parent module relation
min_warning, max_warning, min_critical, max_critical no Numeric thresholds
min_warning_forced, max_warning_forced, min_critical_forced, max_critical_forced no Forced threshold variants
str_warning, str_critical no String-match thresholds
str_warning_forced, str_critical_forced no Forced string thresholds
warning_inverse, critical_inverse no Invert threshold logic
min, max no Valid data range
post_process no Multiplier applied by the server
disabled no Create disabled
status no Force status
timestamp no Override data timestamp
custom_id no Custom identifier
critical_instructions, warning_instructions, unknown_instructions no Operator instructions
quiet no Quiet mode
min_ff_event, min_ff_event_normal, min_ff_event_warning, min_ff_event_critical no FlipFlop thresholds
module_ff_interval, ff_type, ff_timeout, each_ff no FlipFlop behavior
crontab no Cron-style module scheduling
extra_data no Extra payload
alert_templates no List of alert template names to bind

transfer: — delivery

Field Required Description
mode yes tentacle, local or agent_plugin
tentacle.address for tentacle Pandora server address
tentacle.port no (default 41121) Tentacle port
tentacle.binary no Path to tentacle_client if not in $PATH
tentacle.extra_args no Extra tentacle client arguments (list)
local.directory for local Directory where the .data XML file is written
module_prefix_separator no (default " - ") Separator for multi-agent prefixing in agent_plugin mode
Mode Output Use case
tentacle Full <agent_data> XML sent via tentacle client Remote execution (cron, Discovery)
local Full <agent_data> XML written to a directory Running on the Pandora server itself (point it at the incoming dir), or debugging
agent_plugin Only <module> fragments printed to stdout — the real agent adds the header Running as module_plugin of a software agent

agent_plugin specifics:


Creating your own templates

A template is just YAML. Build it top-down: decide what to measure (tasks), how to extract the numbers (filters), which modules to expose (agents/modules), and how to deliver (transfer).

Step 1 — Start from a shipped example

templates/local_commands_example.yml is the shortest complete template and demonstrates all three regexp result shapes. Copy it and replace the tasks:

cp templates/local_commands_example.yml templates/my_template.yml

Step 2 — Write the tasks

Each task runs one command or one HTTP request and stores its (optionally filtered) output in a variable.

tasks:
  # A scalar: one value, no filter (trimmed raw output is stored).
  - name: uptime_seconds
    type: local
    local: { command: "awk '{print int($1)}' /proc/uptime" }
    variable: uptime

  # A scalar via a single-capture regexp.
  - name: load_avg
    type: local
    local: { command: "cat /proc/loadavg" }
    filter:
      type: regexp
      expression: '^(\d+\.\d+)'
    variable: load1

  # An array of objects via named capture groups.
  - 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

Step 3 — Turn variables into modules

A variable that is a scalar feeds one module directly; an array feeds for_each blocks (one module per element).

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

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

  # One module per filesystem; value.* reads the named capture groups.
  - for_each: filesystems
    when: 'value.fstype != "tmpfs"'
    name: "DiskUsed_{{ value.mount }}"
    type: generic_data
    data: "{{ value.pct }}"
    unit: "%"

Inside a for_each block:

Step 4 — Pick the transfer mode

transfer:
  mode: agent_plugin            # modules-only output; run as a software-agent plugin
transfer:
  mode: tentacle                # full agent XML to a Pandora server
  tentacle: { address: "pandora.example.com" }
transfer:
  mode: local                   # full agent XML written to a directory
  local: { directory: "/var/spool/pandora/data_in" }

Step 5 — Validate before shipping

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

--dry-run prints the generated XML without transferring; -v shows which variables were stored and which modules were skipped. The parser is strict, so any typo in a field name fails immediately.

Common pitfalls


Expression language quick reference

{{ }} placeholders and when: conditions are expr-lang expressions. Useful built-ins for template authors:

Category Built-ins / operators
Strings 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 "re2-pattern"
Collections 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)
Logic & nil a ?? "default" (nil coalescing) · obj?.field (optional chaining) · cond ? a : b (ternary) · 'key' in map (key presence)
Numbers + - * / % · abs(x) · min/max(a, b) · sum(list) · avg(list) · int(x) · float(x) · string(x)
JSON toJSON(x) · fromJSON(s)

In filter/map/all/any/none predicates the current element is # (e.g. filter(filesystems, #.pct > 90)).

Do not confuse languages: the task filter.expression is not expr — it is jq (via gojq) or an RE2 regexp, depending on filter.type. expr is only used inside {{ }} and in when:.


Worked example: replacing a real plugin

templates/df_used.yml replaces the pandora_df_used_go agent plugin. The pattern to learn: one execution → array of objects → N modules.

tasks:
  - name: df
    type: local
    local: { command: "df -kTP" }
    filter:
      type: regexp
      # Named groups: each df row becomes {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

The regexp captures all df rows; the default fstype allow-list lives in when: so runtime params can override it. Note the folded scalar (when: >) and the doubled backslash (\\d): expr string literals process escapes, so \\d delivers a verbatim \d to the regexp engine.

Positional captures example

Use this when you want capture order instead of field names:

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

HTTP API example

templates/http_api_example.yml demonstrates token-auth chaining: a login request stores the session token, and later requests embed it via Authorization: "Bearer {{ token }}".


Template checklist (for humans and AI generators)