CheckCommand parameters: set_if with a string (how to call functions in set_if)

Hey,
I found my problem somewhere in the bugtracker in 2015 but I couldn’t find exactly how it was resolved.
The problem was that

( len("$vmware_exclude$") > 0 ) && ( len("$vmware_include$") == 0 )

would not work as expected.
As a matter of fact I need to do exactly that: set a CheckCommand parameter only if the relevant variables exist.

Also, in order to not have to write the same statements all over again, I tried using a function, like this:

function all_exist(l) {
  # Check if all elements provided in list l not empty
  for (i in l) {
    if (len(i)==0) {
      return false
    }
  }
  return true
}

object CheckCommand "SNMP_PROCESS" {
.....
    "--protocols" = {
#      set_if = {{ all_exist["$snmpv3_authprot$", "$snmpv3_privprot$"] }}
      value = "$snmpv3_authprot$,$snmpv3_privprot$"
    }
.....

But it’s not accepting that. (“Error: Argument is not a callable object.”)
In case this has been solved, how do I do this correctly nowadays?
Thanks,
Marki

You’re missing the macro resolving here, actually you’re just passing strings with dollars around. macro() is what you’re looking for.

Not much to change in your algorithm, already very good. I’ve only added more telling variable names, I think, short characters are barely readable. The macro() call is new with the extra variable.

The function needs to be registered in the global namespace, otherwise you cannot call it from set_if.

globals.all_exist = function (args) {
  # Check if all elements provided in list l not empty
  for (unresolvedArg in args) {
    var resolvedArg = macro(unresolvedArg)

    if (len(resolvedArg)==0) {
      return false
    }
  }
  return true
}

The call to set_if also needs to be adjusted a bit with () brackets to pass the function argument.

set_if = {{ all_exist(["$snmpv3_authprot$", "$snmpv3_privprot$"]) }}

Cheers,
Michael

2 Likes

Hey,

I thought this was working fine… at first glance.

However, I was restructuring our config to use more of this feature, and I am now getting:

Location: in /etc/icinga2/zones.d/global-templates/commands.conf: 518:23-518:42
/etc/icinga2/zones.d/global-templates/commands.conf(516):   # Check if all elements provided in list not empty
/etc/icinga2/zones.d/global-templates/commands.conf(517):   for (unresolvedArg in args) {
/etc/icinga2/zones.d/global-templates/commands.conf(518):     var resolvedArg = macro(unresolvedArg)
                                                                                ^^^^^^^^^^^^^^^^^^^^
/etc/icinga2/zones.d/global-templates/commands.conf(519):
/etc/icinga2/zones.d/global-templates/commands.conf(520):     if (len(resolvedArg)==0) {

        (0) Executing check for object 'host!HTTP_Web'

The debugger gives me this:

<4> => args
[ "$postdata$" ]

If I use set_if = {{ len(macro("$postdata$"))>0 }} instead of calling the function, it seems to work fine (at least there are no errors).

This is the CheckCommand:

object CheckCommand "HTTP" {
  import "plugin-check-command"
  command = [ PluginDir + "/check_http" ]

  arguments = {
...
    "--post" = {
      value = "$postdata$"
      set_if = {{ all_exist(["$postdata$"]) }}
    }
...
}

The service definition is not worth mentioning. It currently does not contain “vars.postdata”, which is why we have the “set_if” check in the command in the first place.

Please show the full function definition from /etc/icinga2/zones.d/global-templates/commands.conf.

It’s exactly what you told me to do above:

globals.all_exist = function (args) {
  # Check if all elements provided in list not empty
  for (unresolvedArg in args) {
    var resolvedArg = macro(unresolvedArg)

    if (len(resolvedArg)==0) {
      return false
    }
  }
  return true
}

It’s as if the “macro” function didn’t exist in that scope.

As a matter of fact I have tried with a vanilla Icinga setup and it does the same. Feel free to reproduce by adding the following into /etc/icinga2/conf.d/xyz.conf of a vanilla install.

globals.all_exist = function (args) {
  # Check if all elements provided in list not empty
  for (unresolvedArg in args) {
    var resolvedArg = macro(unresolvedArg)

    if (len(resolvedArg)==0) {
      return false
    }
  }
  return true
}

object CheckCommand "COM_HTTP" {
  import "plugin-check-command"
  command = [ PluginDir + "/check_http" ]

  arguments = {
    "-H" = {
      value = "$address$"
      required = true
    }
    "-u" = {
      value = "$url$"
      set_if = {{ all_exist(["$url$"]) }}
    }
    "-s" = {
      value = "$string$"
      set_if = "$string$"
    }
    "-S" = {
      set_if = "$use_ssl$"
    }
    "-p" = {
      value = "$port$"
      set_if = "$port$"
    }
    "-P" = {
      value = "$login$"
      set_if = {{ all_exist(["$login$"]) }}
    }
    "-t" = {
      value = "$timer$"
      set_if = "$timer$"
    }
    "-f" = "follow"
  }
  if (vars.use_ssl) {
    if (vars.port) {
      arguments += {
        "-p" = {
          value = "$port$"
        }
      }
    } else {
      arguments += {
        "-p" = {
          value = "443"
        }
      }
    }
  }
}

object Host "test" {
  import "generic-host"
  address = "1.2.3.4"
  vars.service.check_http = {
    "http" = {
    }
  }
}

apply Service "HTTP" for (var k => var v in host.vars.service.check_http) {
  import "generic-service"
  check_command = "COM_HTTP"
  display_name = "HTTP_"+k

  assign where host.vars.service.check_http
}

Maybe I was wrong with the assumption in the global function scope then, I did not test it. Better modify the algorithm like this:

globals.all_exist = function (args) {
  # Check if all elements provided in list not empty
  for (arg in args) {
        if (len(arg)==0) {
      return false
    }
  }
  return true
}
set_if = {{ 
  var args = [ macro("$url$") ]
  all_exist(args)
}}
1 Like

Okay, is that a feature or a bug?

Related question: https://icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/ explains the following: “Whenever a host/service object sets the http_sni custom variable to true, the parameter is added to the command line.” It uses the following example:

command = [ PluginDir + "/check_http"]
arguments = {
  "--sni" = {
    set_if = "$http_sni$"
  }
}

Below that, it tells use to use:

command = [ PluginContribDir + "/check_postgres.pl" ]
arguments = {
  "-H" = {
    value = "$postgres_host$"
    set_if = {{ macro("$postgres_unixsocket$") == false }}
    description = "hostname(s) to connect to; defaults to none (Unix socket)"
}

Why can’t you just use in that case:

set_if = "$postgres_unixsocket$"

since it seems to be of type boolean (in this case)?

I’m still trying to fully grasp this concept of macros :wink:

None of them, it is a way of how the scope works. macro() only works when called inside a command, where the resolve lists are populated. This isn’t the case anywhere else inside the DSL scope. I had forgotten about this while putting that into the function itself.

Very good question.

The example with http and the boolean value just does a lookup on the objects, first it looks on the service, then host, then checkcommand. The latter may have set a default value. One of them should evaluate into a value.

If no value is specified, it still resolve to boolean false. That’s all just fine and common best practice what you should use.

The problem with the PostgreSQL command is that

  • host
  • unixsocket

are two different configuration methods for the plugin. Either you pass the -H parameter and tell the plugin to connect to an IP address, or you set the unixsocket variable to avoid setting the -H parameter, and just use this parameter.

Setting both collides and renders the plugin in failure. In order to keep just one CheckCommand, and not the two modes in 2 different CheckCommand objects, this is enabled like this.

  • Whenever postgres_host is used, postgres_unixsocket must be false.
  • If postgres_unixsocket is true, -H is not added to the command line.

So with the power of the DSL and runtime lambda functions for set_if, we can still solve this scenario. Pretty much advanced and therefore hidden in the template library. No-one expects you to fully understand it, just use it :wink:

Cheers,
Michael

1 Like

Forget about it :smiley: First year computer science class fail :frowning: My question was so stupid… It’s obviously about the expression macro("$postgres_unixsocket$") == false evaluating to true, meaning set_if is true if the macro’s not set. So the intention is to have the exact inverse case of what you normally have (we normally set the parameter if the value exists). My bad, I should have looked more carefully at the very special check in question.

Is there any chance that macros will be eliminated in the long run and you will just be able to use service.vars.bla etc as usual instead of macro("$bla$") ? :smiley:

Hmmm there’s currently no plans to remove the runtime macros, as they allow specific different value overrides e.g. when the attribute is not define on the service, it will be picked from the host automatically.

Also, users are aware of it and not many are using advanced techniques like you. If that’s subject to change, maybe in future real major versions, 3 or 4 or whatever version schema we will be using.

In terms of the boolean question, ask yourself why it uses a runtime lambda function and no direct access :wink: