DSL: Get host objects in hostgroup with get_objects() and Array#filter (deep-dive into lambda expressions, functions and closures)

Question

Get all hosts which belong to a specific host group inside the Icinga 2 DSL. This should be used for clustered checks combining multiple location based results.

You can either write a custom function which loops over all host objects, fetching all of them with an if condition. Or you’ll abstract that in existing function calls like shown below.

At first, we need the groupname as string, or fetched from an existing object like this:

var myhost = macro("$host.name$")
log(myhost)
var myobj = get_host(myhost)
var mygroup = myobj.vars.aggregate_group
log(mygroup)

Then a filter should be applied, which takes a callback function, checking for hosts which belong to the mygroup hostgroup.

var nodes = get_objects(Host).filter(node => mygroup in node.groups)

This doesn’t work as described here: Monitoring same hosts from multiple locations - #9 by thoomas

Introduction into Lambdas and Scopes

The following construct looks legit in the first place.

var mygroup = "linux-servers"
var nodes = get_objects(Host).filter(node => mygroup in node.groups)
  • get_objects() returns a list of objects, each element represents a Host object
  • filter() loops over each element and calls the function callback provided
  • the function callback uses an abbreviated lambda function with x => inner function body

Try that on your fresh installation where NodeName as host, and linux-servers as hostgroup exists.

icinga2 console --connect 'https:/root:icinga@localhost:5665/'
<1> => var mygroup = "linux-servers"
<2> => var nodes = get_objects(Host).filter(node => mygroup in node.groups)

It fails, but why?

Lambda functions and scoped variables

The lambda expression

node => mygroup in node.groups

can also be written as

f = function(node) { mygroup in node.groups }

f as function name is ignored here, since the definition and call happens in one place inside filter() making this an anonymous function.

A call to this function will fail since mygroup isn’t defined in the local function scope.

Example from icinga2 console --connect ...:

<25>: var nodes = get_objects(Host).filter(node => mygroup in node.groups)
                                                   ^^^^^^^
Error while evaluating expression: Tried to access undefined script variable 'mygroup'

Let’s try this again with the function we’ve defined before:

<26> => f = function(node) { mygroup in node.groups }
null
<27> => get_objects(Host).filter(f)

<26>: f = function(node) { mygroup in node.groups }
                           ^^^^^^^
Error while evaluating expression: Tried to access undefined script variable 'mygroup'

So, how can we solve this problem?

Bind scopes into functions: Closures

It’s possible to bind specific variables from the caller’s scope into the called function with closures.

This requires the use keyword followed after the function parameter list. Hint: Therefore it cannot be used in combination with lambda expressions.

Take the example from above, and bind the mygroup variable into the function’s scope:

f = function(node) use(mygroup) { mygroup in node.groups }

Now call this function in the filter() function.

get_objects(Host).filter(f)

It works!

Solution

You can combine this into a runtime function like this:

vars.dummy_text = {{

  var myhost = macro("$host.name$")
  log(myhost)
  var myobj = get_host(myhost)
  var mygroup = myobj.vars.aggregate_group
  log(mygroup)

  var f = function(node) use(mygroup) { mygroup in node.groups }
  var nodes = get_objects(Host).filter(f)

  //cluster checks
  //...
}}

Continue here for a deep-dive into clustered checks: Advanced Topics - Icinga 2

References

The above will get easier with 2.12 and closures for passing variables into the lambda scope.