Skip to content

Lua Scripts

NSClient++ can host Lua scripts in-process via the LUAScript module. Hosted scripts can call NSClient++’s APIs directly — run checks, listen on channels, register new check commands, read and write settings — and they keep state between invocations.

To add a Lua script as an internal script you need to load the LUAScript module and register the script:

nscp lua add --script my_script.lua

This produces:

[/modules]
LUAScript = enabled

[/settings/lua/scripts]
my_script = my_script.lua

The key is the script’s alias, and add uses the file name without .lua unless you pass --alias my_script. Scripts are resolved against ${scripts} (typically scripts/lua/) and may be specified with or without the .lua extension. A lib/ folder under scripts/lua/ is added to package.path automatically, so shared helpers can live in scripts/lua/lib/.

Managing scripts

Command What it does
nscp lua list [--json] [--include-lib] List the files under ${scripts}/lua; helpers in a lib folder only with --include-lib
nscp lua add --script <file> [--alias <name>] Configure a script and enable the module; --no-config leaves the configuration alone
nscp lua add --script <file> --import <path> [--replace] Copy a script into ${scripts}/lua first; an existing file is only overwritten with --replace
nscp lua show --script <file> Print a script
nscp lua delete --script <file> Delete a script and every [/settings/lua/scripts] entry that loads it
nscp lua install --add <file> / --remove <file> Add or remove a configured script
nscp lua execute --script <file> [args...] Run a script’s main (see below)

A script added this way loads the next time LUAScript loads or reloads. show and delete only reach ${scripts}/lua; a name that resolves anywhere else is refused, and so is a path through a symlinked folder that leads out of it. show reads a symlink only when its target is inside the folder too. delete removes the entry in the folder - a file, or a symlink itself, whatever it points at, dangling or not - and never what a link points to. A deleted script stays loaded until the module reloads. The same operations are available over REST under /api/v2/scripts/lua.

Lifecycle

A Lua script can hook into three lifecycle moments:

  • Top-level code — runs once when the script is loaded. Use this to register check commands, channel subscriptions, and event handlers.
  • on_start — optional global function, invoked once every script and every module has loaded. Use it for work that needs other modules to be ready.
  • main — optional global function, invoked when the script is run from the command line.

A script that does not parse, or whose top-level code raises an error, is logged and never started: its on_start does not run, though whatever its top-level code registered before the error stays registered. An on_start that raises is logged. Either way the other scripts load and start regardless.

A reload of the module (Core():reload("LUAScript"), or a reload of the service) loads every script afresh: the top-level code runs again in a new Lua state, so any state a script kept in its variables starts over, and on_start runs again once the reload is complete - on a reload of the service, after any module the same reload enabled has loaded. Commands and channels a script no longer registers - because it was deleted, or changed - are gone after the reload.

Top-level code

The body of the script runs exactly once, when the LUAScript module loads it. This is where you register everything the script wants to expose:

local function check_hello(command, args)
    return "ok", "Hello from Lua!", ""
end

Registry():simple_query("check_hello", check_hello, "A Lua greeting")

After the script is loaded, NSClient++ will route check_hello to your function.

on_start

Define a global on_start() function if you need a hook that fires after all scripts have been loaded — e.g. to talk to other modules or schedule background work.

function on_start()
    nscp.info("Lua script ready")
end

on_start takes no arguments and does not need to return anything.

main

main is invoked when the script is run from the command line:

nscp lua execute --script my_script.lua install --root /tmp

The script is loaded as a second copy for the run - its top-level code runs, on_start does not - and main receives the command-line arguments as an array (Lua table). It must return a 2-tuple (code, message); a script with no main fails with Failed to handle command main:

function main(args)
    for i, v in ipairs(args) do
        nscp.info(string.format("arg %d = %s", i, v))
    end
    return "ok", "Ran with " .. #args .. " arguments"
end

Status codes

Throughout the API, status codes are represented as strings:

String Meaning
"ok" Nagios OK
"warning" Nagios warning
"critical" Nagios critical
"unknown" Nagios unknown

The wrapper also accepts the corresponding integer codes (0/1/2/3) on input, but always produces strings when handing values to your callbacks. Prefer the string form in your own code. Any other value - a string not in the table, or an integer outside 0-3 - is read as "unknown".

Errors in handlers

A handler that raises an error does not take the agent down. The caller gets an answer it can act on, and the error is logged:

Handler Raises an error Returns no status
simple_query (a check) unknown, Failed to handle command: <command>: <error> unknown, Invalid return from <command>: expected (code, message, perf)
simple_cmdline Failed to handle command: <command>: <error>, non-zero exit Invalid return from <command>: expected (code, message), non-zero exit
simple_subscription the submission fails with Failed to handle channel: <channel>: <error> the submission fails

The message and the performance data are optional: a handler that returns only "warning" is a warning with an empty message.

A call into the API with too few arguments raises a Lua error naming the expected syntax (for example Incorrect syntax: simple_query(command, args)), which the script can catch with pcall.

API

The Lua environment exposes:

  • A nscp global with utility functions (info, print, error, sleep, getSetting)
  • Three constructors that return wrapper objects: Core(), Registry(), Settings()
  • Standard Lua library functions (string, table, io, os, etc.)

Wrapper objects use method-call syntax (obj:method(args)). The : is required — calling with . will not pass the object instance correctly.

local core = Core()
local code, msg, perf = core:simple_query("check_cpu", {})

Most operations have a simple variant that uses plain Lua values, and a raw variant that takes serialized protobuf messages. Use the simple form unless you need fields it doesn’t expose.

The nscp global

nscp.info / nscp.print / nscp.error

nscp.info(message)
nscp.print(message)   -- alias for nscp.info
nscp.error(message)

Write a message to the NSClient++ log. info and print log at the info level; error logs at the error level.

nscp.info("Script starting up")
nscp.error("Something is wrong: " .. err)

nscp.sleep

nscp.sleep(milliseconds)

Sleep for the given number of milliseconds. Use this rather than busy-looping or shelling out to os.execute("sleep ...").

nscp.info("Waiting 500ms")
nscp.sleep(500)

nscp.getSetting

value = nscp.getSetting(path, key, default)

One-shot helper for reading a single string setting without instantiating Settings().

local port = nscp.getSetting("/settings/NRPE/server", "port", "5666")

For anything more involved, instantiate Settings() directly (see below).

Core

Core() returns a wrapper around the running NSClient++ instance — use it to run check commands, submit passive results, and reload modules.

local core = Core()

Core:simple_query

code, message, perf = core:simple_query(command, args)

Run a check command. args can be a Lua table of argument strings or a single argument string. Returns the Nagios status string, the message, and the performance data. A command nobody registered returns "unknown" and Unknown command(s): <command>.

local code, msg, perf = core:simple_query("check_cpu", {"warn=load > 80", "crit=load > 90"})
nscp.info(string.format("%s: %s (%s)", code, msg, perf))

Core:query_target

code, message, perf = core:query_target(target, command, args)

Like simple_query, but the request names target - one configured for a client module, such as [/settings/NRPE/client/targets/<target>] - in its header. A client module’s query command that is given no target= of its own, such as nrpe_query, runs on the target named there. args can be a Lua table or a single string.

local code, msg = Core():query_target("backup-server", "nrpe_query", {"command=check_cpu"})

Only a command that takes its target from the request does anything with it: any other command - one a module or script on this agent serves - runs here as simple_query would, whatever target says. To run an arbitrary command on a remote agent, use query_forward.

Core:query_forward

code, message, perf = core:query_forward(forward_command, target, command, args)

Relay a command to a target through a client module’s forwarding command (nrpe_forward for NRPE). Unlike query_target, the command and its arguments are sent on the wire exactly as given, so the remote agent receives them unchanged. Returns the remote answer.

local code, msg, perf = Core():query_forward("nrpe_forward", "backup-server", "check_drivesize", {"drive=C:"})

Core:query

ok, response_bytes = core:query(request_bytes)

Raw protobuf variant of simple_query. request_bytes is a serialized QueryRequestMessage; response_bytes is a serialized QueryResponseMessage. Use Core:create_pb_query to build the request bytes from a command and argument list.

Core:create_pb_query

request_bytes = core:create_pb_query(command, args)

Build a serialized QueryRequestMessage for Core:query. args can be a Lua table or a single string.

local req = core:create_pb_query("check_cpu", {"warn=load > 80"})
local ok, resp = core:query(req)

Core:simple_exec

code, results = core:simple_exec(target, command, args)

Execute a command-line command - one a script registered with Registry:simple_cmdline, or one a module provides. args is a Lua table. target picks the module to run it in:

target Runs the command in
a module name or alias that module ("LUAScript", "CheckSystem")
"any" the first module that has the command
"all" or "*" every module that has the command

The name is matched as a substring of each module’s name. Avoid "": it matches every module as though each had been named. code is the status string the command answered with, and results a Lua array of strings, one per module that answered. When nothing could run it - no such module, or no such command in it - code is "unknown" and results holds the reason, Failed to execute <command> on <target>.

local code, lines = core:simple_exec("LUAScript", "say_hello", {"world"})
for _, line in ipairs(lines) do nscp.info(line) end

Core:simple_submit

ok, response = core:simple_submit(channel, command, code, message, perf)

Submit a passive check result on a channel (e.g. "NSCA", "NRDP").

Argument Description
channel Channel to submit to
command Check command name being reported
code Status string ("ok", "warning", …)
message Message text
perf Performance data string

ok is true when every handler on the channel accepted the result, and response is what it answered. A handler that rejects it makes ok false, with the handler’s message; a channel nobody listens on returns false and Failed to submit message: <channel>.

core:simple_submit("NSCA", "check_battery", "warning", "Battery low (15%)", "")

Core:reload

core:reload(module)

Reload the given module by name. Pass "service" to reload the entire service. A script that asks for its own module ("LUAScript") to be reloaded gets its answer first: the core runs the reload once the call has returned, and the script then starts over as described under Lifecycle.

Core:log

core:log([level], message)

Log a message at the specified level, or at "info" when only the message is given. level is a string: "info", "error", "debug", etc. The line is attributed to the script and the line that called log. nscp.info() / nscp.error() are usually more convenient.

Not implemented

Core:exec and Core:submit, the raw protobuf variants of simple_exec and simple_submit, are not implemented: calling one raises the Lua error Unsupported API called: Core:exec (or Core:submit). Use the simple variants.

Registry

Registry() lets a script publish itself into NSClient++ — registering check commands, command-line commands, and channel subscriptions.

local reg = Registry()

Registration happens at the top level of the script; once registered, your callbacks fire whenever the corresponding command is invoked.

Registry:simple_query / Registry:simple_function

reg:simple_query(name, function, description)
reg:simple_function(name, function, description)   -- alias

Register a check command with a simple callback. Both names refer to the same registration helper — simple_query reads more naturally for queries.

Argument Description
name The check command name (e.g. check_hello)
function Callback invoked when the command runs
description Help text shown by <command> help and the WEB UI

The callback signature is (command, args) -> (code, message, perf):

local function check_hello(command, args)
    return "ok", "Hello!", "'count'=1;5;10"
end

Registry():simple_query("check_hello", check_hello, "Returns a greeting")

Registry:query

reg:query(name, function, description)

Register a check command with a raw, protobuf-based callback. Use simple_query instead unless you need fields it doesn’t expose.

The callback signature is (command, request_bytes, request_message_bytes) -> response_bytes, where response_bytes is a serialized QueryResponseMessage.

Registry:simple_cmdline

reg:simple_cmdline(name, function, description)

Register a command-line command, run with nscp client --module LUAScript --exec <name> [args...] or from a script with Core:simple_exec. Callback signature is (command, args) -> (code, message).

The module’s own verbs come first, so a handler registered under one of their names is never reached: help, execute, lua-script, lua-run, add, install, list, show and delete.

local function do_something(command, args)
    return "ok", "Did " .. (args[1] or "nothing")
end

Registry():simple_cmdline("do_something", do_something, "Sample cmdline command")

Registry:simple_subscription

reg:simple_subscription(channel, function, description)

Subscribe to a channel (think passive check submissions). Per submission, the callback receives the channel, the originating command name, the status code, and a Lua table of {message = perf} lines:

local function on_submit(channel, command, code, lines)
    for msg, perf in pairs(lines) do
        nscp.info(string.format("%s on %s [%s]: %s", command, channel, code, msg))
    end
    return true, "ok"
end

Registry():simple_subscription("MY-CHANNEL", on_submit, "Custom submission handler")

Return (success_bool, message). Returning false fails the submission.

To route real-time submissions through your handler, point a filter or client at the channel name you registered:

[/modules]
LUAScript=enabled
CheckEventLog=enabled

[/settings/eventlog/real-time/filters/login]
log=Security
filter=id=4624
target=MY-CHANNEL

Registry:cmdline / Registry:subscription

The raw protobuf variants of simple_cmdline and simple_subscription are not implemented: calling one raises the Lua error Unsupported API called: Registry:cmdline (or Registry:subscription).

Settings

Settings() wraps the configuration store. Reads return current values; writes are in-memory only until :save() is called.

local config = Settings()

Settings:get_section

keys = config:get_section(path)

Return the keys under a given section as a Lua array of strings.

for _, key in ipairs(Settings():get_section("/modules")) do
    nscp.info("Module: " .. key)
end

Settings:get_string / Settings:set_string

value = config:get_string(path, key, default)
config:set_string(path, key, value)

Read or write a string. Writes are not persisted until :save() is called.

local config = Settings()
local existing = config:get_string("/modules", "LUAScript", "disabled")
config:set_string("/modules", "LUAScript", "enabled")
config:save()

Settings:get_bool / Settings:set_bool

value = config:get_bool(path, key, default)
config:set_bool(path, key, value)

Read or write a boolean. true, 1 and yes read as true; any other value reads as false, and default is returned only when the key is not set.

Settings:get_int / Settings:set_int

value = config:get_int(path, key, default)
config:set_int(path, key, value)

Read or write an integer. A value that is not a number reads as default.

local port = Settings():get_int("/settings/NRPE/server", "port", 5666)
nscp.info("NRPE port is: " .. port)

Settings:save

config:save()

Persist any in-memory changes back to the settings store.

Settings:register_path

config:register_path(path, title, description)

Register a settings section for documentation and WEB UI purposes.

Settings:register_key

config:register_key(path, key, type, title, description, default)

Register an individual settings key. type is a type hint string ("string", "int", "bool").

local config = Settings()
config:register_path("/settings/my_script", "My Lua script", "Configuration for my Lua script")
config:register_key("/settings/my_script", "interval", "int",
                    "Sampling interval",
                    "How often, in seconds, to sample",
                    "60")

A complete example

A script that registers two checks and a passive submission handler:

-- scripts/lua/example.lua

-- A simple active check
local function check_random(command, args)
    local n = math.random(0, 100)
    if n > 90 then
        return "critical", "Random value " .. n .. " is too high", "'random'=" .. n .. ";80;90"
    elseif n > 80 then
        return "warning", "Random value " .. n .. " is high", "'random'=" .. n .. ";80;90"
    else
        return "ok", "Random value " .. n .. " is fine", "'random'=" .. n .. ";80;90"
    end
end

-- A passive submission handler that logs every submission
local function on_submit(channel, command, code, lines)
    for msg, _ in pairs(lines) do
        nscp.info(string.format("[%s] %s %s: %s", channel, command, code, msg))
    end
    return true, "ok"
end

-- A cmdline command for ad-hoc invocation
local function say_hello(command, args)
    return "ok", "Hello, " .. (args[1] or "world")
end

local reg = Registry()
reg:simple_query("check_random", check_random, "Random-number sanity check")
reg:simple_subscription("LOG-SUBMIT", on_submit, "Log every submitted result")
reg:simple_cmdline("say_hello", say_hello, "Greet someone")

function on_start()
    nscp.info("example.lua ready")
end

function main(args)
    nscp.info("Invoked from the command line with " .. #args .. " arguments")
    return "ok", "done"
end

Enable it via:

[/modules]
LUAScript = enabled

[/settings/lua/scripts]
example = example.lua