Under the hood, Icinga 2 uses many constants and reserved keywords, e.g. “Critical” or “Zone” which are respected by the config parser and compiler. This sometimes leads to errors when users accidentally override such things, or re-define their own global constants. v2.10 introduces namespaces for this purpose, and ensures that such accidents won’t happen anymore.

 

What exactly is a namespace?

Think of a defined “room” for variables, functions, etc. which can be defined similar to constants. They are isolated from other namespaces and must be loaded by the user/developer. Namespaces need a defined name – we prefer to use a capitalized string, e.g. “MyNamespace”. This helps to immediately qualify this as customization when reading the configuration.

Icinga 2 v2.10 uses namespaces which are registered and loaded by default. In order to test this, open the debug console on a test VM which has the snapshot packages installed. Fetch the keys in global namespace “globals” first.

icinga2 console

<1> => keys(globals)
[ "Environment", "Icinga", "Internal", "MaxConcurrentChecks", "NodeName", "StatsFunctions", "System", "Types" ]

“Environment”, “MaxConcurrentChecks”, “NodeName” are special global constants, the other keys are registered namespaces. You can access specific constants either via namespace prefix, or without.

<2> => System.ApplicationVersion
"v2.9.1-180-gc9db7a0"
<3> => PlatformName
"CentOS Linux"

Coming from v2.9 and older, nothing changes if you’re using global constants and functions.

There’s one safety warning in place though: When you’re overriding a constant at some later point, the configuration compiler will log a warning including the file and line details.

 

What’s in there for me?

Think of your own safe place for constants and functions.

v2.9 introduced a global constant called “Environment” which will be used for TLS SNI handling in an addon. This broke an existing Icinga setup (no-one reads upgrading docs ;)). In addition to that, global functions are currently registered in this “cumbersome” syntax:

globals.functionname = function(param1, param2) { ... }

while the documentation on functions suggests to rather write

function functionname(param1, param2) { ... }

if we would have a namespace. With v2.10, you have and I’ll show you in a bit how to solve this.

 

Create a namespace

Prepare the configuration and include the newly created “namespaces.conf” file in your “icinga2.conf” configuration file.

vim /etc/icinga2/icinga2.conf

  include "constants.conf"
+ include "namespaces.conf"

Next, edit “namespaces.conf” and add a namespace with the unique name

vim /etc/icinga2/namespaces.conf"

namespace ApplyFunctionsBE {
  Mysql.databases["production"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
  Mysql.databases["staging"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
}

This is safe, and when you now open the debug console, the namespace isn’t used globally yet.

 

Use a namespace

Namespaces can be loaded into the current scope with the “using” keyword. Edit “namespaces.conf” again and add the following after the namespace declaration. Note: This loads the namespace immediately. You can also move this into a specific configuration file on top – think of Python package imports for example.

vim /etc/icinga2/namespaces.conf

using ApplyFunctionsBE

Now restart Icinga 2 and use the debug console to connect to the running instance via REST API.

systemctl restart icinga2

ICINGA2_API_USERNAME=root ICINGA2_API_PASSWORD=icinga icinga2 console --connect 'https://localhost:5665/'

<1> => ApplyFunctionsBE
{
	Mysql = {
		databases = {
....
		}
	}
}

The above example shows you how to access an attribute registered in this namespace. You can use that in your configuration objects and apply rules then. Thanks to loading the namespace, you can directly access the namespace variables too.

 

Add a function to a namespace

Next to specific variables in namespaces, you might want to register functions in a specific namespace. This keeps the global namespace safe and no-one is allowed to override this function by accident.

Go with a simple example which flattens a nested dictionary like this (e.g. from a host “vars” dictionary).

  databases["production"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
  databases["staging"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }

to

  result["production-app1"] = { "port" = 3306, "location" = "BE" }
  result["production-app2"] = { "port" = 3307, "location" = "BE" }
  result["staging-app1"] = { "port" = 3306, "location" = "BE" }
  result["staging-app2"] = { "port" = 3307, "location" = "BE" }

The algorithm is easy, the function rather short, and it has a namespace specific custom attribute inside. I’ve hacked up this function in 10 minutes, it includes error handling and logging. Feel free to adopt or enhance :)

/* namespace begin: ApplyFunctionsBE. */
namespace ApplyFunctionsBE {
  const MysqlUsername = "icinga-be"

  /* Flatten a dictionary with unique keys for service apply for rules. */
  function flattenDictionary2ndLevel(d) {
    var res = {}

    if (typeof(d) != Dictionary) {
      log(LogWarning, "config", "ERROR: flattenDictionary2ndLevel parameter is not a dictionary.")
      return {} // return an empty result set on error, including the log above
    }

    for (k1 => v1 in d) {
      if (typeof(v1) != Dictionary) {
        log(LogWarning, "config", "ERROR: First level with key " + k1 + " is not a dictionary value.")
        continue;
      }

      for (k2 => v2 in v1) {
        var new_key = k1 + "-" + k2 /* Generate a unique name. */
        var new_val = v2.clone()

        /* Add a namespace specific variable. */
        new_val["username"] = MysqlUsername /* This pulls in the namespace local constant. */

        res[new_key] = new_val
      }
    }

    return res
  }
  /* This is for testing only. Move this into an actual host object. */
  Mysql.databases["production"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
  Mysql.databases["staging"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }

/* namespace end: ApplyFunctionsBE. */
}

/* Load namespaces. */
using ApplyFunctionsBE

Validate the configuration, restart Icinga 2 and test again with the debug console. This results in the following output:

<8> => ApplyFunctionsBE.flattenDictionary2ndLevel(ApplyFunctionsBE.Mysql.databases)
{
	"production-app1" = {
		port = 3306.000000
		username = "icinga-be"
	}
	"production-app2" = {
		port = 3307.000000
		username = "icinga-be"
	}
	"staging-app1" = {
		port = 3306.000000
		username = "icinga-be"
	}
	"staging-app2" = {
		port = 3307.000000
		username = "icinga-be"
	}
}

Note: The debug console requires to explicitly load the namespace again for tests. The configuration snippet above already takes care for the config compiler.

<9> => using ApplyFunctionsBE; flattenDictionary2ndLevel(Mysql.databases)

A configuration example can look like this:

object Host "namespace01" {
  check_command = "dummy"

  vars.mysql_databases["production"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
  vars.mysql_databases["staging"] = { "app1" = { "port" = 3306 }, "app2" = { "port" = 3307 } }
}

/* This explicitly accesses the namespace. */
//apply Service "mysql-" for (db_key => config in ApplyFunctionsBE.flattenDictionary2ndLevel(host.vars.mysql_databases)) {
/* This requires using the namespace globally. */
apply Service "mysql-" for (db_key => config in flattenDictionary2ndLevel(host.vars.mysql_databases)) {
  check_command = "dummy"

  vars += config
}

 

What’s next?

Use the snapshot packages (e.g. inside the Vagrant boxes) and play around with the namespaces. Find a suitable use case for you and share your ideas and findings on the community channels :)

v2.10 will be released end of September. Special thanks to Gunnar for implementing this nifty feature.