Setting grains dynamically
As you already know, grains hold variables that describe certain aspects of a Minion. This could be information about the operating system, the hardware, the network, and so on. It can also contain statically defined user data, which is configured either in /etc/salt/minion or /etc/salt/grains. It is also possible to define grains dynamically using grains modules.
Setting some basic grains
Grains modules are interesting in that so long as the module is loaded, all public functions will be executed. As each function is executed, it will return a dictionary, which contains items to be merged into the Minion's grains.
Let's go ahead and set up a new grains module to demonstrate. We'll prepend the names of the return data with a z so that it is easy to find.
''' Test module for Extending SaltStack This module should be saved as salt/grains/testdata.py ''' def testdata(): ''' Return some test data ''' return {'ztest1': True}
Go ahead and save this file as salt/grains/testdata.py, and then use salt-call to display all of the grains, including this one:
# salt-call --local grains.items local: ---------- ... virtual: physical zmqversion: 4.1.3 ztest1: True
Keep in mind that you can also use grains.item to display only a single grain:
# salt-call --local grains.item ztest local: ---------- ztest1: True
It may not look like this module is much good, since this is still just static data that could be defined in the minion or grains files. But keep in mind that, as with other modules, grains modules can be gated using a __virtual__() function. Let's go ahead and set that up, along with a flag of sorts that will determine whether or not this module will load in the first place:
import os.path def __virtual__(): ''' Only load these grains if /tmp/ztest exists ''' if os.path.exists('/tmp/ztest'): return True return False
Go ahead and run the following commands to see this in action:
# salt-call --local grains.item ztest local: ---------- ztest: # touch /tmp/ztest # salt-call --local grains.item ztest local: ---------- ztest: True
This is very useful for gating the return data from an entire module, whether dynamic or, as this module currently is, static.
You may be wondering why that example checked for the existence of a file, rather than checking the existing Minion configuration. This is to illustrate that the detection of certain system properties is likely to dictate how grains are set. If you want to just set a flag inside the minion file, you can pull it out of __opts__. Let's go ahead and add that to the __virtual__() function:
def __virtual__(): ''' Only load these grains if /tmp/ztest exists ''' if os.path.exists('/tmp/ztest'): return True if __opts__.get('ztest', False): return True return False
Go ahead and remove the old flag, and set the new one:
# rm /tmp/ztest # echo 'ztest: True' >> /etc/salt/minion # salt-call --local grains.item ztest local: ---------- ztest: True
Let's go ahead and set up this module to return dynamic data as well. Because YAML is so prevalent in Salt, let's go ahead and set up a function that returns the contents of a YAML file:
import yaml import salt.utils def yaml_test(): ''' Return sample data from /etc/salt/test.yaml ''' with salt.utils.fopen('/etc/salt/yamltest.yaml', 'r') as fh_: return yaml.safe_load(fh_)
You may notice that we've used salt.utils.fopen() instead of a standard Python open(). Salt's fopen() function wraps Python's open() with some extra handling, so that files are closed properly on Minions.
Save your module, and then issue the following commands to see the result:
# echo 'yamltest: True' > /etc/salt/yamltest.yaml # salt-call --local grains.item yamltest local: ---------- yamltest: True
(Not) cross-calling execution modules
You may be tempted to try to cross-call an execution module from inside a grains module. Unfortunately, that won't work. The __virtual__() function in many execution modules relies heavily on grains. Allowing grains to cross-call to execution modules, before Salt has decided whether or not to even the execution module in the first place, would cause circular dependencies.
Just remember, grains are loaded first, then pillars, then execution modules. If you have code that you plan to use two or more of these types of modules, consider setting up a library for it in the salt/utils/ directory.
The final grains module
With all of the code we've put together, the resulting module should look like the following:
''' Test module for Extending SaltStack. This module should be saved as salt/grains/testdata.py ''' import os.path import yaml import salt.utils def __virtual__(): ''' Only load these grains if /tmp/ztest exists ''' if os.path.exists('/tmp/ztest'): return True if __opts__.get('ztest', False): return True return False def testdata(): ''' Return some test data ''' return {'ztest1': True} def yaml_test(): ''' Return sample data from /etc/salt/test.yaml ''' with salt.utils.fopen('/etc/salt/yamltest.yaml', 'r') as fh_: return yaml.safe_load(fh_)