Skip to content

Python Scripts

In addition to executing Python scripts as regular external scripts, NSClient++ can also execute them in-process via the PythonScript module. The benefit is that scripts can call NSClient++'s APIs directly (run checks, listen on channels, register handlers, read/write settings) and keep state between invocations.

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

nscp py add --script my_script.py

This produces:

[/modules]
PythonScript = enabled

[/settings/python/scripts]
my_script = my_script.py

Lifecycle functions

A script may define any of three lifecycle functions:

  • __main__ — invoked when the script is run from the command line
  • init — invoked when the script is loaded by the module
  • shutdown — invoked when the module unloads or NSClient++ stops

All three are optional. If a function is missing it is simply skipped.

__main__

__main__ is invoked when the script is run from the command line. Use it for installation, configuration helpers, or anything else an administrator might want to run interactively:

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

The function receives the command-line arguments as a list of strings:

from NSCP import log

def __main__(args):
    log("My arguments are: %s" % args)

Output:

nscp py execute --script my_script.py install --root /tmp
L     python My arguments are: ['install', '--root', '/tmp']

init

init is invoked when the script is loaded and is where you obtain the values needed to talk back to NSClient++. It receives three arguments:

  • plugin_id — internal identifier of the plugin instance; required by Registry.get(), Core.get(), and Settings.get()
  • plugin_alias — the alias the PythonScript module is loaded under
  • script_alias — the key the script is registered under in [/settings/<alias>/scripts]
from NSCP import log

def init(plugin_id, plugin_alias, script_alias):
    log("*** plugin_id: %d, plugin_alias: %s, script_alias: %s"
        % (plugin_id, plugin_alias, script_alias))

A typical startup log looks like:

D     python boot python
D     python Prepare python
D     python init python
D     python Adding script: osmc (c:\source\build\x64\dev\scripts\python\osmc.py)
D     python Loading python script: c:\source\build\x64\dev\scripts\python\osmc.py
D     python Lib path: c:\source\build\x64\dev\scripts\python\lib
L     python *** plugin_id: 0, plugin_alias: python, script_alias: my_script

plugin_alias versus script_alias

Consider:

[/modules]
ps1 = PythonScript
ps2 = PythonScript

[/settings/ps1/scripts]
ms1 = my_script.py
ms2 = my_script.py

[/settings/ps2/scripts]
ms1 = my_script.py
ms2 = my_script.py

This loads the same script four times with the following arguments:

plugin_id plugin_alias script_alias
1 ps1 ms1
1 ps1 ms2
2 ps2 ms1
2 ps2 ms2

If a script may legitimately be loaded multiple times, store its configuration under a path that includes both aliases, e.g. /settings/<script_name>/<plugin_alias>/<script_alias>/.

Warning

Loading the same script multiple times is discouraged — the CLI does not provide shortcuts for it — but it is supported for complex scenarios.

shutdown

shutdown is invoked when NSClient++ stops or when the PythonScript module is unloaded. It takes no arguments:

from NSCP import log

def shutdown():
    log("Good bye")

API

The module is imported as NSCP and exposes:

  • Three wrapper classes: Settings, Registry, Core — each obtained via .get(plugin_id)
  • An enum: status — Nagios-style return codes
  • Free functions: log, log_error, log_debug, sleep

Most operations have a "simple" variant that uses plain Python types, and a "raw" variant that takes serialized protobuf messages for full access to the underlying API. Use the simple form unless you need fields the simple form doesn't expose.

Free functions

log / log_error / log_debug

log(message)
log_error(message)
log_debug(message)

Write a message to the NSClient++ log at the corresponding level (info, error, debug). Messages appear on the command line and in any configured log file.

from NSCP import log, log_error, log_debug

log("Informational message")
log_error("Something went wrong")
log_debug("Internal state: %s" % state)

sleep

sleep(milliseconds)

Sleep for the given number of milliseconds. Use this instead of time.sleep(): Python is single-threaded with respect to the GIL, so a blocking time.sleep() inside an NSClient++ script prevents every other Python callback from running. sleep() releases the GIL while it waits.

from NSCP import sleep, log_debug

log_debug("Waiting for 1 second")
sleep(1000) # Other taska can no execute while we wait
log_debug("It is now one second later")

status enum

Key Value Description
OK 0 Nagios OK
WARNING 1 Nagios warning
CRITICAL 2 Nagios critical
UNKNOWN 3 Nagios unknown

Registry

Registry lets a script publish itself into NSClient++ — registering check commands, command-line commands, channel subscriptions, event handlers, and metrics providers.

Registry.get

Registry.get(plugin_id)

Obtain an instance of Registry bound to the calling plugin. Pass the plugin_id you received in init.

Registry.function

Registry.function(query_name, query_function, description)

Register a check query with a raw, protobuf-based callback. Use simple_function instead unless you need fields it doesn't expose.

Option Description
query_name The check command name (e.g. check_py_test)
query_function Callback invoked when the query runs
description Help text shown by <command> help and in the WEB UI

The callback receives the command name and a serialized QueryRequestMessage, and must return a 2-tuple (return_code, response_bytes) where response_bytes is a serialized QueryResponseMessage:

def my_function(command, request):
    request_message = plugin_pb2.QueryRequestMessage()
    request_message.ParseFromString(request)
    # ... build response ...
    response_message = plugin_pb2.QueryResponseMessage()
    return (status.OK, response_message.SerializeToString())

Example:

from NSCP import log, Registry, status
import plugin_pb2

def my_function(command, request):
    request_message = plugin_pb2.QueryRequestMessage()
    request_message.ParseFromString(request)
    log('Got command: %s' % request_message.payload[0].command)
    response_message = plugin_pb2.QueryResponseMessage()
    return (status.OK, response_message.SerializeToString())

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.function('check_py_test', my_function, 'This is a sample python function')

Registry.simple_function

Registry.simple_function(query_name, query_function, description)

Register a check query with a simple Python callback — no protobuf required.

Option Description
query_name The check command name
query_function Callback invoked when the query runs
description Help text shown by <command> help

The callback receives the arguments as a list of strings and must return a 3-tuple (status, message, perfdata):

def my_function(args):
    return (status.OK, "This is the message", "'count'=123;200;600")

Example:

from NSCP import log, Registry, status

def my_function(args):
    log('Got arguments: %s' % args)
    return (status.OK, "This is the message", "'count'=123;200;600")

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.simple_function('check_py_test', my_function, 'This is a sample python function')

Registry.cmdline

Registry.cmdline(command_name, function)

Register a command-line command (invoked via nscp client --command ... or nscp ext --command ...) with a protobuf-based callback. Use simple_cmdline unless you need raw access.

Option Description
command_name The command name to expose
function Callback invoked when the command runs

Callback signature: (command, request) -> (return_code, response_bytes), where request is a serialized ExecuteRequestMessage and response_bytes is a serialized ExecuteResponseMessage.

from NSCP import log, Registry, status
import plugin_pb2

def my_function(command, request):
    request_message = plugin_pb2.ExecuteRequestMessage()
    request_message.ParseFromString(request)
    log('Got command: %s' % request_message.payload[0].command)
    response_message = plugin_pb2.ExecuteResponseMessage()
    return (0, response_message.SerializeToString())

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.cmdline('do_foo', my_function)

Registry.simple_cmdline

Registry.simple_cmdline(command_name, function)

Register a command-line command with a simple callback. The callback receives the arguments as a list of strings and returns a 2-tuple (exit_code, message):

def my_function(args):
    return (0, "This is the message")

Example:

from NSCP import log, Registry

def my_function(args):
    log('Got arguments: %s' % args)
    return (1, "Everything is awesome")

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.simple_cmdline('do_something', my_function)

Registry.subscription

Registry.subscription(channel, function)

Subscribe to a channel (think passive check submissions) with a protobuf-based handler. Use simple_subscription unless you need raw access.

Option Description
channel The channel name to subscribe to
function Callback invoked when a message arrives

Callback signature: (channel, message) -> (success_bool, response_bytes). message is a memoryview over a serialized SubmitRequestMessage.

def my_function(channel, message):
    request = plugin_pb2.SubmitRequestMessage()
    request.ParseFromString(bytes(message))
    # ... process ...
    return (True, b"")

Registry.simple_subscription

Registry.simple_subscription(channel, function)

Subscribe to a channel with a simple per-payload callback.

Option Description
channel The channel name to subscribe to
function Callback invoked per submitted check

Callback signature: (channel, source, command, status, message, perf) -> bool. Return True to indicate success.

Example — suppress repeated NSCA submissions:

from NSCP import Registry, Core, status, log

g_last_message = ''
g_plugin_id = 0

def filter_nsca(channel, source, command, code, message, perf):
    global g_last_message, g_plugin_id
    if message != g_last_message:
        g_last_message = message
        log("Sending: %s" % message)
        core = Core.get(g_plugin_id)
        core.simple_submit('NSCA', command, status.CRITICAL,
                           message.encode('utf-8'), perf)
        return True
    else:
        log("Suppressing duplicate message")
        return True

def init(plugin_id, plugin_alias, script_alias):
    global g_plugin_id
    g_plugin_id = plugin_id
    reg = Registry.get(plugin_id)
    reg.simple_subscription('filter_nsca', filter_nsca)

Then point a real-time filter at the synthetic filter_nsca channel instead of NSCA:

[/modules]
PythonScript=enabled
CheckEventLog=enabled
NSCAClient=enabled

[/settings/python/scripts]
filter_nsca=filter_nsca.py

[/settings/eventlog/real-time]
enabled = true

[/settings/eventlog/real-time/filters/login]
log=Security
filter=id=4624
target=filter_nsca
detail syntax=%(id)

Registry.event / Registry.event_pb

Registry.event(event_name, event_function)
Registry.event_pb(event_name, event_function)

Register a handler for a named event. The two variants differ in how the event payload is delivered to the callback:

  • event — payload is delivered as a Python dict. Callback signature: (event_name, data) -> None.
  • event_pb — payload is delivered as a raw bytes buffer (serialized protobuf). Callback signature: (event_name, request_bytes). The return value is ignored.

Example (event):

from NSCP import log, Registry

def on_event(event, data):
    log('Got event: %s with key=%s' % (event, data.get('key')))

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.event('eventlog:login', on_event)

Registry.submit_metrics

Registry.submit_metrics(callable)

Register a function that receives metrics each time NSClient++ publishes a metrics snapshot. Useful for forwarding metrics to a custom backend.

Callback signature: (metrics, "") -> None, where metrics is a flat dict of dotted-name keys to stringified values.

from NSCP import Registry, log

def on_metrics(metrics, _):
    for key, value in metrics.items():
        log("metric %s = %s" % (key, value))

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.submit_metrics(on_metrics)

Registry.fetch_metrics

Registry.fetch_metrics(callable)

Register a function that produces metrics on demand. The callback takes no arguments and returns a dict whose values may be strings, ints, or floats; numeric values are emitted as gauges.

from NSCP import Registry

def my_metrics():
    return {
        "my_script.requests": 42,
        "my_script.latency_ms": 17.3,
        "my_script.status": "ok",
    }

def init(plugin_id, plugin_alias, script_alias):
    reg = Registry.get(plugin_id)
    reg.fetch_metrics(my_metrics)

Registry.query

(return_code, response_bytes) = Registry.query(request_bytes)

Issue a raw query against the registry — used to enumerate modules, queries, channels, etc. The request_bytes must be a serialized RegistryRequestMessage; the response is a serialized RegistryResponseMessage.

Core

The Core object exposes operations that interact with the running NSClient++ instance itself — running queries, submitting passive results, loading and reloading modules, expanding paths.

Core.get

Core.get(plugin_id)

Obtain a Core instance bound to the calling plugin.

Core.simple_query

(code, message, perf) = Core.simple_query(query, arguments)

Run a check command by name with a list of argument strings.

Option Description
query Name of the query to run
arguments List of keyword=value argument strings
code Nagios status code
message Resulting message
perf Resulting performance data
from NSCP import Core, log

def init(plugin_id, plugin_alias, script_alias):
    core = Core.get(plugin_id)
    (code, message, perf) = core.simple_query("check_cpu", [])
    log(message)

Core.query

(return_code, response_bytes) = Core.query(command, request_bytes)

Raw protobuf variant of simple_query. request_bytes is a serialized QueryRequestMessage; response_bytes is a serialized QueryResponseMessage.

from NSCP import Core, log
import plugin_pb2

def init(plugin_id, plugin_alias, script_alias):
    request_message = plugin_pb2.QueryRequestMessage()
    # ... populate request_message ...
    core = Core.get(plugin_id)
    (ret, response) = core.query("check_cpu", request_message.SerializeToString())
    response_message = plugin_pb2.QueryResponseMessage()
    response_message.ParseFromString(response)

Core.simple_exec

(return_code, results) = Core.simple_exec(target, command, arguments)

Execute a command-line command (registered via Registry.cmdline / Registry.simple_cmdline) against target (a remote NSClient++ instance or "local"/"" for in-process). Returns the result as a list of strings.

Core.exec

(return_code, response_bytes) = Core.exec(target, request_bytes)

Raw protobuf variant of simple_exec.

Core.simple_submit

(success, response) = Core.simple_submit(channel, command, code, message, perf)

Submit a passive check result on a channel.

Option Description
channel Channel to submit to (e.g. "NSCA", "NRDP")
command Check command name being reported
code status.OK / status.WARNING / status.CRITICAL / status.UNKNOWN
message Message text (bytes or str)
perf Performance data string
from NSCP import Core, status

def init(plugin_id, plugin_alias, script_alias):
    core = Core.get(plugin_id)
    core.simple_submit("NSCA", "check_py", status.OK,
                       "All good".encode("utf-8"), "")

Core.submit

(success, error_message) = Core.submit(channel, request_bytes)

Raw protobuf variant of simple_submit. request_bytes is a serialized SubmitRequestMessage.

Core.reload

Core.reload(module)

Reload the given module by name (e.g. "CheckEventLog"). Pass "service" to reload the entire service.

Core.load_module

Core.load_module(module, alias)

Load a module into the running NSClient++ instance. alias lets you load the same module multiple times under different names; pass "" for the default.

from NSCP import Core

def init(plugin_id, plugin_alias, script_alias):
    core = Core.get(plugin_id)
    core.load_module("CheckEventLog", "")

Core.unload_module

Core.unload_module(module_or_alias)

Unload a previously loaded module by name or alias.

from NSCP import Core

def init(plugin_id, plugin_alias, script_alias):
    core = Core.get(plugin_id)
    core.unload_module("CheckEventLog")

Core.expand_path

path = Core.expand_path(path_expression)

Expand a path containing ${...} placeholders (e.g. ${scripts}, ${modules}, ${base-path}).

from NSCP import Core, log

def init(plugin_id, plugin_alias, script_alias):
    core = Core.get(plugin_id)
    path = core.expand_path("${scripts}/python/myscript.py")
    log('The script path is: %s' % path)

Settings

Settings wraps the configuration store. Read and write are supported; written values are in-memory only until save() is called.

Settings.get

Settings.get(plugin_id)

Obtain a Settings instance bound to the calling plugin.

from NSCP import Settings

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    value = config.get_string("/modules", "PythonScript", "disabled")

Settings.get_section

keys = Settings.get_section(path)

Return the list of keys under a given section.

from NSCP import Settings, log

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    for key in config.get_section("/modules"):
        log("Module: %s" % key)

Settings.get_string / Settings.set_string

value = Settings.get_string(path, key, default_value)
Settings.set_string(path, key, value)

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

from NSCP import Settings, log

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    for key in config.get_section("/modules"):
        value = config.get_string("/modules", key, "unknown")
        log("The module %s is %s" % (key, value))
from NSCP import Settings

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    config.set_string("/modules", "PythonScript", "enabled")
    config.save()

Settings.get_bool / Settings.set_bool

value = Settings.get_bool(path, key, default_value)
Settings.set_bool(path, key, value)

Read or write a boolean.

from NSCP import Settings

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    config.set_bool("/modules", "PythonScript", True)
    config.save()

Settings.get_int / Settings.set_int

value = Settings.get_int(path, key, default_value)
Settings.set_int(path, key, value)

Read or write an integer.

from NSCP import Settings, log

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    port = config.get_int("/settings/NRPE/server", "port", -1)
    log("NRPE port is: %d" % port)
    config.set_int("/settings/NRPE/server", "port", 1234)
    config.save()

Settings.save

Settings.save()

Persist any in-memory changes back to the settings store (file or registry, depending on configuration).

Settings.register_path

Settings.register_path(path, title, description)

Register a settings section so it is documented in the generated settings file and visible in the WEB UI.

Settings.register_key

Settings.register_key(path, key, type, title, description, default_value)

Register an individual settings key for documentation and UI purposes. The type argument is accepted for forward-compatibility, but the underlying store currently treats all script-registered keys as strings.

Settings.query

(return_code, response_bytes) = Settings.query(request_bytes)

Issue a raw settings query. request_bytes is a serialized SettingsRequestMessage; the response is a SettingsResponseMessage. Use this when you need operations that the typed helpers above don't cover (bulk reads, diffs, deletions, etc.).

from NSCP import Settings
import plugin_pb2

def init(plugin_id, plugin_alias, script_alias):
    config = Settings.get(plugin_id)
    request_message = plugin_pb2.SettingsRequestMessage()
    # ... populate request_message ...
    (ret, response) = config.query(request_message.SerializeToString())
    response_message = plugin_pb2.SettingsResponseMessage()
    response_message.ParseFromString(response)