mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 08:58:59 +00:00
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:
commit
153087a7b4
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user