SaltStack: Creating a Custom Grain using Python

saltstack_logo-thumbnailSaltStack grains are used for relatively static information such as operating system, IP address, and other system properties.  They are also useful for targeting minions, for example whether a system is part of  dev/test/prod, or a flag on whether it falls under LifeScience or HIPAA regulation.

In this article we will implement a custom grain that determines whether a host is part of development, test, or production environment based on a simplistic naming scheme.   This custom grain will be written in Python.

As opposed to Salt custom external pillars which are evaluated on the Master, custom grains are gathered/evaluated on the minion, so be aware that any custom libraries you import into the Python script would need to be installed on the minion evaluating it.

Solution Design

The algorithm we will use for determining whether a host is part of a development, test, or production deployment will be intentionally simple.  If the hostname contains ‘-dev’, we consider it a development box, if it contains ‘-test’ we consider it a test box, and if it contains ‘-prod’ we consider it part of production.   For example, a system with a hostname of ‘my-test.mydomain.com’ would be considered a TEST box.

I know this solution is contrived, but the purpose of this example is only to illustrate the integration hooks for custom grain development.

Based on the hostname, we will then return a grain named ‘envtype’ that will have a value of ‘dev’, ‘test’, ‘prod’, or ‘n/a’ if it cannot be determined.

Create Custom Grain

On the Salt Master, create ‘envtypegrain.py’ in a specially named directory named ‘_grains’ under the main Salt file root as shown below:

> mkdir -p /srv/salt/_grains
> touch /srv/salt/_grains/envtypegrain.py
> chmod 755 /srv/salt/_grains/envtypegrain.py

Use this content for envtypegrain.py:

#!/usr/bin/python

import socket
import logging
log = logging.getLogger(__name__)

def environment():

    hostname = socket.gethostname().upper()
    log.debug("envtypegrain hostname: " + hostname)

    if "-DEV" in hostname:
        return {'envtype':'dev'}
    elif "-TEST" in hostname:
        return {'envtype':'test'}
    elif "-PROD" in hostname:
        return {'envtype':'prod'}
    else:
        return {'envtype':'n/a'}



if __name__ == "__main__":
    print environment()

Remember that grains are evaluated not on the master, but at each minion, so go to the console of one of the minions and have it synchronize the latest grains and then retrieve the value of the grain named ‘envtype’:

> salt-call saltutil.sync_grains
> salt-call grains.item envtype

Of course, you could have run this from the Salt Master as well, targeting the minion, but I wanted to emphasize that the grain evaluation (i.e. python execution of the script) is done on the minion side.

If you ran this from a minion with a hostname of ‘my-test.mydomain.com’, it would return back:

local:
  -----------
  envtype: 
      test

And if you opened the minion log at ‘/var/log/salt/minion’, you would see a log message containing the phrase ‘envtypegrain hostname: my-test.mydomain.com’ which is sent via the logger in the Python script.

This custom grain value can now be used when targeting hosts or in custom states as appropriate for your needs.

 

REFERENCES

https://docs.saltstack.com/en/latest/topics/grains/

http://dev.mlsdigital.net/posts/SaltStackBeyondJinjaStates/

https://www.digitalocean.com/community/tutorials/an-introduction-to-saltstack-terminology-and-concepts