Merge pull request #26945 from dr4Ke/feature_state_grains_support_nested_and_dict

Feature state grains support nested and dict
This commit is contained in:
Colton Myers 2015-10-15 14:25:44 -06:00
commit 153087a7b4
4 changed files with 903 additions and 108 deletions

View File

@ -15,6 +15,7 @@ import collections
from functools import reduce
# Import 3rd-party libs
from salt.utils.odict import OrderedDict
import yaml
import salt.ext.six as six
from salt.ext.six.moves import range # pylint: disable=import-error,no-name-in-module,redefined-builtin
@ -86,7 +87,7 @@ def get(key, default='', delimiter=DEFAULT_TARGET_DELIM):
pkg:apache
delimiter
:param delimiter:
Specify an alternate delimiter to use when traversing a nested dict
.. versionadded:: 2014.7.0
@ -239,6 +240,8 @@ def setvals(grains, destructive=False):
# Cast defaultdict to dict; is there a more central place to put this?
yaml.representer.SafeRepresenter.add_representer(collections.defaultdict,
yaml.representer.SafeRepresenter.represent_dict)
yaml.representer.SafeRepresenter.add_representer(OrderedDict,
yaml.representer.SafeRepresenter.represent_dict)
cstr = yaml.safe_dump(grains, default_flow_style=False)
try:
with salt.utils.fopen(gfn, 'w+') as fp_:
@ -295,9 +298,10 @@ def append(key, val, convert=False, delimiter=DEFAULT_TARGET_DELIM):
is given. Defaults to False.
:param delimiter: The key can be a nested dict key. Use this parameter to
specify the delimiter you use.
specify the delimiter you use, instead of the default ``:``.
You can now append values to a list in nested dictionnary grains. If the
list doesn't exist at this level, it will be created.
.. versionadded:: 2014.7.6
CLI Example:
@ -329,24 +333,39 @@ def append(key, val, convert=False, delimiter=DEFAULT_TARGET_DELIM):
return setval(key, grains)
def remove(key, val):
def remove(key, val, delimiter=DEFAULT_TARGET_DELIM):
'''
.. versionadded:: 0.17.0
Remove a value from a list in the grains config file
:param delimiter: The key can be a nested dict key. Use this parameter to
specify the delimiter you use, instead of the default ``:``.
You can now append values to a list in nested dictionnary grains. If the
list doesn't exist at this level, it will be created.
.. versionadded:: Boron
CLI Example:
.. code-block:: bash
salt '*' grains.remove key val
'''
grains = get(key, [])
grains = get(key, [], delimiter)
if not isinstance(grains, list):
return 'The key {0} is not a valid list'.format(key)
if val not in grains:
return 'The val {0} was not in the list {1}'.format(val, key)
grains.remove(val)
while delimiter in key:
key, rest = key.rsplit(delimiter, 1)
_grain = get(key, None, delimiter)
if isinstance(_grain, dict):
_grain.update({rest: grains})
grains = _grain
return setval(key, grains)
@ -538,7 +557,7 @@ def get_or_set_hash(name,
.. warning::
This function could return strings which may contain characters which are reserved
as directives by the YAML parser, such as strings beginning with `%`. To avoid
as directives by the YAML parser, such as strings beginning with ``%``. To avoid
issues when using the output of this function in an SLS file containing YAML+Jinja,
surround the call with single quotes.
'''
@ -569,7 +588,7 @@ def set(key,
with nested keys.
This function is conservative. It will only overwrite an entry if
its value and the given one are not a list or a dict. The `force`
its value and the given one are not a list or a dict. The ``force``
parameter is used to allow overwriting in all cases.
.. versionadded:: 2015.8.0
@ -579,7 +598,8 @@ def set(key,
:param destructive: If an operation results in a key being removed,
delete the key, too. Defaults to False.
:param delimiter:
Specify an alternate delimiter to use when traversing a nested dict
Specify an alternate delimiter to use when traversing a nested dict,
the default being ``:``
CLI Example:
@ -589,7 +609,7 @@ def set(key,
salt '*' grains.set 'apps:myApp' '{port: 2209}'
'''
ret = {'comment': [],
ret = {'comment': '',
'changes': {},
'result': True}
@ -600,19 +620,21 @@ def set(key,
elif isinstance(val, list):
_new_value_type = 'complex'
_existing_value = get(key, _non_existent_key, delimiter)
_non_existent = object()
_existing_value = get(key, _non_existent, delimiter)
_value = _existing_value
_existing_value_type = 'simple'
if _existing_value == _non_existent_key:
if _existing_value is _non_existent:
_existing_value_type = None
elif isinstance(_existing_value, dict):
_existing_value_type = 'complex'
elif isinstance(_existing_value, list):
_existing_value_type = 'complex'
if _existing_value_type is not None and _existing_value == val:
ret['comment'] = 'The value \'{0}\' was already set for key \'{1}\''.format(val, key)
if _existing_value_type is not None and _existing_value == val \
and (val is not None or destructive is not True):
ret['comment'] = 'Grain is already set'
return ret
if _existing_value is not None and not force:

View File

@ -10,58 +10,89 @@ file on the minions, by default at: /etc/salt/grains
Note: This does NOT override any grains set in the minion file.
'''
from __future__ import absolute_import
from salt.defaults import DEFAULT_TARGET_DELIM
import re
def present(name, value):
def present(name, value, delimiter=DEFAULT_TARGET_DELIM, force=False):
'''
Ensure that a grain is set
.. versionchanged:: Boron
name
The grain name
value
The value to set on the grain
If the grain with the given name exists, its value is updated to the new value.
If the grain does not yet exist, a new grain is set to the given value.
:param force: If force is True, the existing grain will be overwritten
regardless of its existing or provided value type. Defaults to False
.. versionadded:: Boron
:param delimiter: A delimiter different from the default can be provided.
.. versionadded:: Boron
It is now capable to set a grain to a complex value (ie. lists and dicts)
and supports nested grains as well.
If the grain does not yet exist, a new grain is set to the given value. For
a nested grain, the necessary keys are created if they don't exist. If
a given key is an existing value, it will be converted, but an existing value
different from the given key will fail the state.
If the grain with the given name exists, its value is updated to the new
value unless its existing or provided value is complex (list or dict). Use
`force: True` to overwrite.
.. code-block:: yaml
cheese:
grains.present:
- value: edam
nested_grain_with_complex_value:
grains.present:
- name: icinga:Apache SSL
- value:
- command: check_https
- params: -H localhost -p 443 -S
with,a,custom,delimiter:
grains.present:
- value: yay
- delimiter: ,
'''
name = re.sub(delimiter, DEFAULT_TARGET_DELIM, name)
ret = {'name': name,
'changes': {},
'result': True,
'comment': ''}
if isinstance(value, dict):
ret['result'] = False
ret['comment'] = 'Grain value cannot be dict'
return ret
if __grains__.get(name) == value:
_non_existent = object()
existing = __salt__['grains.get'](name, _non_existent)
if existing == value:
ret['comment'] = 'Grain is already set'
return ret
if __opts__['test']:
ret['result'] = None
if name not in __grains__:
if existing is _non_existent:
ret['comment'] = 'Grain {0} is set to be added'.format(name)
ret['changes'] = {'new': name}
else:
ret['comment'] = 'Grain {0} is set to be changed'.format(name)
ret['changes'] = {'new': name}
ret['changes'] = {'changed': {name: value}}
return ret
grain = __salt__['grains.setval'](name, value)
if grain != {name: value}:
ret['result'] = False
ret['comment'] = 'Failed to set grain {0}'.format(name)
return ret
ret['result'] = True
ret['changes'] = grain
ret['comment'] = 'Set grain {0} to {1}'.format(name, value)
ret = __salt__['grains.set'](name, value, force=force)
if ret['result'] is True and ret['changes'] != {}:
ret['comment'] = 'Set grain {0} to {1}'.format(name, value)
ret['name'] = name
return ret
def list_present(name, value):
def list_present(name, value, delimiter=DEFAULT_TARGET_DELIM):
'''
.. versionadded:: 2014.1.0
@ -73,6 +104,10 @@ def list_present(name, value):
value
The value is present in the list type grain.
:param delimiter: A delimiter different from the default ``:`` can be provided.
.. versionadded:: Boron
The grain should be `list type <http://docs.python.org/2/tutorial/datastructures.html#data-structures>`_
.. code-block:: yaml
@ -91,6 +126,8 @@ def list_present(name, value):
- web
- dev
'''
name = re.sub(delimiter, DEFAULT_TARGET_DELIM, name)
ret = {'name': name,
'changes': {},
'result': True,
@ -129,7 +166,7 @@ def list_present(name, value):
ret['comment'] = 'Failed append value {1} to grain {0}'.format(name, value)
return ret
else:
if value not in __grains__.get(name):
if value not in __salt__['grains.get'](name, delimiter=DEFAULT_TARGET_DELIM):
ret['result'] = False
ret['comment'] = 'Failed append value {1} to grain {0}'.format(name, value)
return ret
@ -138,7 +175,7 @@ def list_present(name, value):
return ret
def list_absent(name, value):
def list_absent(name, value, delimiter=DEFAULT_TARGET_DELIM):
'''
Delete a value from a grain formed as a list.
@ -150,6 +187,10 @@ def list_absent(name, value):
value
The value to delete from the grain list.
:param delimiter: A delimiter different from the default ``:`` can be provided.
.. versionadded:: Boron
The grain should be `list type <http://docs.python.org/2/tutorial/datastructures.html#data-structures>`_
.. code-block:: yaml
@ -168,11 +209,13 @@ def list_absent(name, value):
- web
- dev
'''
name = re.sub(delimiter, DEFAULT_TARGET_DELIM, name)
ret = {'name': name,
'changes': {},
'result': True,
'comment': ''}
grain = __grains__.get(name)
grain = __salt__['grains.get'](name, None)
if grain:
if isinstance(grain, list):
if value not in grain:
@ -198,7 +241,10 @@ def list_absent(name, value):
return ret
def absent(name, destructive=False):
def absent(name,
destructive=False,
delimiter=DEFAULT_TARGET_DELIM,
force=False):
'''
.. versionadded:: 2014.7.0
@ -210,17 +256,53 @@ def absent(name, destructive=False):
:param destructive: If destructive is True, delete the entire grain. If
destructive is False, set the grain's value to None. Defaults to False.
:param force: If force is True, the existing grain will be overwritten
regardless of its existing or provided value type. Defaults to False
.. versionadded:: Boron
:param delimiter: A delimiter different from the default can be provided.
.. versionadded:: Boron
.. versionchanged:: Boron
This state now support nested grains and complex values. It is also more
conservative: if a grain has a value that is a list or a dict, it will
not be removed unless the `force` parameter is True.
.. code-block:: yaml
grain_name:
grains.absent
grains.absent: []
'''
_non_existent = object()
name = re.sub(delimiter, DEFAULT_TARGET_DELIM, name)
ret = {'name': name,
'changes': {},
'result': True,
'comment': ''}
if name in __grains__:
grain = __salt__['grains.get'](name, _non_existent)
if grain is None:
if __opts__['test']:
ret['result'] = None
if destructive is True:
ret['comment'] = 'Grain {0} is set to be deleted'\
.format(name)
ret['changes'] = {'deleted': name}
return ret
ret = __salt__['grains.set'](name,
None,
destructive=destructive,
force=force)
if ret['result']:
if destructive is True:
ret['comment'] = 'Grain {0} was deleted'\
.format(name)
ret['changes'] = {'deleted': name}
ret['name'] = name
elif grain is not _non_existent:
if __opts__['test']:
ret['result'] = None
if destructive is True:
@ -232,20 +314,27 @@ def absent(name, destructive=False):
'deleted (None)'.format(name)
ret['changes'] = {'grain': name, 'value': None}
return ret
__salt__['grains.delval'](name, destructive)
if destructive is True:
ret['comment'] = 'Grain {0} was deleted'.format(name)
ret['changes'] = {'deleted': name}
else:
ret['comment'] = 'Value for grain {0} was set to {1}'\
.format(name, None)
ret['changes'] = {'grain': name, 'value': None}
ret = __salt__['grains.set'](name,
None,
destructive=destructive,
force=force)
if ret['result']:
if destructive is True:
ret['comment'] = 'Grain {0} was deleted'\
.format(name)
ret['changes'] = {'deleted': name}
else:
ret['comment'] = 'Value for grain {0} was set to None' \
.format(name)
ret['changes'] = {'grain': name, 'value': None}
ret['name'] = name
else:
ret['comment'] = 'Grain {0} does not exist'.format(name)
return ret
def append(name, value, convert=False):
def append(name, value, convert=False,
delimiter=DEFAULT_TARGET_DELIM):
'''
.. versionadded:: 2014.7.0
@ -261,17 +350,22 @@ def append(name, value, convert=False):
If convert is False and the grain contains non-list contents, an error
is given. Defaults to False.
:param delimiter: A delimiter different from the default can be provided.
.. versionadded:: Boron
.. code-block:: yaml
grain_name:
grains.append:
- value: to_be_appended
'''
name = re.sub(delimiter, DEFAULT_TARGET_DELIM, name)
ret = {'name': name,
'changes': {},
'result': True,
'comment': ''}
grain = __grains__.get(name)
grain = __salt__['grains.get'](name, None)
if grain:
if isinstance(grain, list):
if value in grain:

View File

@ -292,23 +292,21 @@ class GrainsModuleTestCase(TestCase):
grainsmod.__grains__ = {'a': 12, 'c': 8}
res = grainsmod.set('a', 12)
self.assertTrue(res['result'])
self.assertEqual(res['comment'], 'The value \'12\' was already set for key \'a\'')
self.assertEqual(res['comment'], 'Grain is already set')
self.assertEqual(grainsmod.__grains__, {'a': 12, 'c': 8})
# Set a grain to the same complex value
grainsmod.__grains__ = {'a': ['item', 12], 'c': 8}
res = grainsmod.set('a', ['item', 12])
self.assertTrue(res['result'])
self.assertEqual(res['comment'], 'The value \'[\'item\', 12]\' was already set '
+ 'for key \'a\'')
self.assertEqual(res['comment'], 'Grain is already set')
self.assertEqual(grainsmod.__grains__, {'a': ['item', 12], 'c': 8})
# Set a key to the same simple value in a nested grain
grainsmod.__grains__ = {'a': 'aval', 'b': {'nested': 'val'}, 'c': 8}
res = grainsmod.set('b,nested', 'val', delimiter=',')
self.assertTrue(res['result'])
self.assertEqual(res['comment'], 'The value \'val\' was already set for key '
+ '\'b,nested\'')
self.assertEqual(res['comment'], 'Grain is already set')
self.assertEqual(grainsmod.__grains__, {'a': 'aval',
'b': {'nested': 'val'},
'c': 8})

File diff suppressed because it is too large Load Diff