mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
States can have sub state returns
This commit is contained in:
parent
fef492e04d
commit
f909cb5aaa
5 changed files with 261 additions and 17 deletions
1
changelog/57993.added
Normal file
1
changelog/57993.added
Normal file
|
@ -0,0 +1 @@
|
|||
- Added the ability for states to return `sub_state_run`s -- results frome external state engines
|
|
@ -259,6 +259,45 @@ A State Module must return a dict containing the following keys/values:
|
|||
|
||||
States should not return data which cannot be serialized such as frozensets.
|
||||
|
||||
Sub State Runs
|
||||
--------------
|
||||
|
||||
Some states can return multiple state runs from an external engine.
|
||||
State modules that extend tools like Puppet, Chef, Ansible, and idem can run multiple external
|
||||
states and then return their results individually in the "sub_state_run" portion of their return
|
||||
as long as their individual state runs are formatted like salt states with low and high data.
|
||||
|
||||
For example, the idem state module can execute multiple idem states
|
||||
via it's runtime and report the status of all those runs by attaching them to "sub_state_run" in it's state return.
|
||||
These sub_state_runs will be formatted and printed alongside other salt states.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
state_return = {
|
||||
"name": None, # The parent state name
|
||||
"result": None, # The overall status of the external state engine run
|
||||
"comment": None, # Comments on the overall external state engine run
|
||||
"changes": {}, # An empty dictionary, each sub state run has it's own changes to report
|
||||
"sub_state_run": [
|
||||
{
|
||||
"changes": {}, # A dictionary describing the changes made in the external state run
|
||||
"result": None, # The external state run name
|
||||
"comment": None, # Comment on the external state run
|
||||
"duration": None, # Optional, the duration in seconds of the external state run
|
||||
"start_time": None, # Optional, the timestamp of the external state run's start time
|
||||
"low": {
|
||||
"name": None, # The name of the state from the external state run
|
||||
"state": None, # Name of the external state run
|
||||
"__id__": None, # ID of the external state run
|
||||
"fun": None, # The Function name from the external state run
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
Test State
|
||||
==========
|
||||
|
||||
|
|
|
@ -3071,6 +3071,22 @@ class State:
|
|||
running[tag] = self.call(low, chunks, running)
|
||||
if tag in running:
|
||||
self.event(running[tag], len(chunks), fire_event=low.get("fire_event"))
|
||||
|
||||
for sub_state_data in running[tag].pop("sub_state_run", ()):
|
||||
self.__run_num += 1
|
||||
sub_tag = _gen_tag(sub_state_data["low"])
|
||||
running[sub_tag] = {
|
||||
"name": sub_state_data["low"]["name"],
|
||||
"changes": sub_state_data["changes"],
|
||||
"result": sub_state_data["result"],
|
||||
"duration": sub_state_data.get("duration"),
|
||||
"start_time": sub_state_data.get("start_time"),
|
||||
"comment": sub_state_data.get("comment"),
|
||||
"__state_ran__": True,
|
||||
"__run_num__": self.__run_num,
|
||||
"__sls__": low["__sls__"],
|
||||
}
|
||||
|
||||
return running
|
||||
|
||||
def call_listen(self, chunks, running):
|
||||
|
|
|
@ -26,26 +26,36 @@ class OutputUnifier(object):
|
|||
else:
|
||||
self.policies.append(getattr(self, pls))
|
||||
|
||||
def _run_policies(self, data):
|
||||
for pls in self.policies:
|
||||
try:
|
||||
data = pls(data)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
log.debug(
|
||||
"An exception occurred in this state: %s",
|
||||
exc,
|
||||
exc_info_on_loglevel=logging.DEBUG,
|
||||
)
|
||||
data = {
|
||||
"result": False,
|
||||
"name": "later",
|
||||
"changes": {},
|
||||
"comment": "An exception occurred in this state: {0}".format(exc),
|
||||
}
|
||||
return data
|
||||
|
||||
def __call__(self, func):
|
||||
def _func(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
for pls in self.policies:
|
||||
try:
|
||||
result = pls(result)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
log.debug(
|
||||
"An exception occurred in this state: %s",
|
||||
exc,
|
||||
exc_info_on_loglevel=logging.DEBUG,
|
||||
)
|
||||
result = {
|
||||
"result": False,
|
||||
"name": "later",
|
||||
"changes": {},
|
||||
"comment": "An exception occurred in this state: {0}".format(
|
||||
exc
|
||||
),
|
||||
}
|
||||
sub_state_run = None
|
||||
if isinstance(result, dict):
|
||||
sub_state_run = result.get("sub_state_run", ())
|
||||
result = self._run_policies(result)
|
||||
if sub_state_run:
|
||||
result["sub_state_run"] = [
|
||||
self._run_policies(sub_state_data)
|
||||
for sub_state_data in sub_state_run
|
||||
]
|
||||
return result
|
||||
|
||||
return _func
|
||||
|
@ -77,6 +87,9 @@ class OutputUnifier(object):
|
|||
if err_msg:
|
||||
raise SaltException(err_msg)
|
||||
|
||||
for sub_state in result.get("sub_state_run", ()):
|
||||
self.content_check(sub_state)
|
||||
|
||||
return result
|
||||
|
||||
def unify(self, result):
|
||||
|
|
|
@ -601,6 +601,49 @@ class StateCompilerTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
|
|||
]
|
||||
self.assertEqual(run_num, 0)
|
||||
|
||||
def test_call_chunk_sub_state_run(self):
|
||||
"""
|
||||
Test running a batch of states with an external runner
|
||||
that returns sub_state_run
|
||||
"""
|
||||
low_data = {
|
||||
"state": "external",
|
||||
"name": "external_state_name",
|
||||
"__id__": "do_a_thing",
|
||||
"__sls__": "external",
|
||||
"order": 10000,
|
||||
"fun": "state",
|
||||
}
|
||||
mock_call_return = {
|
||||
"__run_num__": 0,
|
||||
"sub_state_run": [
|
||||
{
|
||||
"changes": {},
|
||||
"result": True,
|
||||
"comment": "",
|
||||
"low": {
|
||||
"name": "external_state_name",
|
||||
"__id__": "external_state_id",
|
||||
"state": "external_state",
|
||||
"fun": "external_function",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
expected_sub_state_tag = "external_state_|-external_state_id_|-external_state_name_|-external_function"
|
||||
with patch("salt.state.State._gather_pillar") as state_patch:
|
||||
with patch("salt.state.State.call", return_value=mock_call_return):
|
||||
minion_opts = self.get_temp_config("minion")
|
||||
minion_opts["disabled_requisites"] = ["require"]
|
||||
state_obj = salt.state.State(minion_opts)
|
||||
ret = state_obj.call_chunk(low_data, {}, {})
|
||||
sub_state = ret.get(expected_sub_state_tag)
|
||||
assert sub_state
|
||||
self.assertEqual(sub_state["__run_num__"], 1)
|
||||
self.assertEqual(sub_state["name"], "external_state_name")
|
||||
self.assertEqual(sub_state["__state_ran__"], True)
|
||||
self.assertEqual(sub_state["__sls__"], "external")
|
||||
|
||||
|
||||
class HighStateTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
|
||||
def setUp(self):
|
||||
|
@ -871,6 +914,138 @@ class StateReturnsTestCase(TestCase):
|
|||
assert statedecorators.OutputUnifier("unify")(lambda: data)()["result"] is False
|
||||
|
||||
|
||||
@skipIf(pytest is None, "PyTest is missing")
|
||||
class SubStateReturnsTestCase(TestCase):
|
||||
"""
|
||||
TestCase for code handling state returns.
|
||||
"""
|
||||
|
||||
def test_sub_state_output_check_changes_is_dict(self):
|
||||
"""
|
||||
Test that changes key contains a dictionary.
|
||||
:return:
|
||||
"""
|
||||
data = {"changes": {}, "sub_state_run": [{"changes": []}]}
|
||||
out = statedecorators.OutputUnifier("content_check")(lambda: data)()
|
||||
assert "'Changes' should be a dictionary" in out["sub_state_run"][0]["comment"]
|
||||
assert not out["sub_state_run"][0]["result"]
|
||||
|
||||
def test_sub_state_output_check_return_is_dict(self):
|
||||
"""
|
||||
Test for the entire return is a dictionary
|
||||
:return:
|
||||
"""
|
||||
data = {"sub_state_run": [["whatever"]]}
|
||||
out = statedecorators.OutputUnifier("content_check")(lambda: data)()
|
||||
assert (
|
||||
"Malformed state return. Data must be a dictionary type"
|
||||
in out["sub_state_run"][0]["comment"]
|
||||
)
|
||||
assert not out["sub_state_run"][0]["result"]
|
||||
|
||||
def test_sub_state_output_check_return_has_nrc(self):
|
||||
"""
|
||||
Test for name/result/comment keys are inside the return.
|
||||
:return:
|
||||
"""
|
||||
data = {"sub_state_run": [{"arbitrary": "data", "changes": {}}]}
|
||||
out = statedecorators.OutputUnifier("content_check")(lambda: data)()
|
||||
assert (
|
||||
" The following keys were not present in the state return: name, result, comment"
|
||||
in out["sub_state_run"][0]["comment"]
|
||||
)
|
||||
assert not out["sub_state_run"][0]["result"]
|
||||
|
||||
def test_sub_state_output_unifier_comment_is_not_list(self):
|
||||
"""
|
||||
Test for output is unified so the comment is converted to a multi-line string
|
||||
:return:
|
||||
"""
|
||||
data = {
|
||||
"sub_state_run": [
|
||||
{
|
||||
"comment": ["data", "in", "the", "list"],
|
||||
"changes": {},
|
||||
"name": None,
|
||||
"result": "fantastic!",
|
||||
}
|
||||
]
|
||||
}
|
||||
expected = {
|
||||
"sub_state_run": [
|
||||
{
|
||||
"comment": "data\nin\nthe\nlist",
|
||||
"changes": {},
|
||||
"name": None,
|
||||
"result": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
assert statedecorators.OutputUnifier("unify")(lambda: data)() == expected
|
||||
|
||||
data = {
|
||||
"sub_state_run": [
|
||||
{
|
||||
"comment": ["data", "in", "the", "list"],
|
||||
"changes": {},
|
||||
"name": None,
|
||||
"result": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
expected = "data\nin\nthe\nlist"
|
||||
assert (
|
||||
statedecorators.OutputUnifier("unify")(lambda: data)()["sub_state_run"][0][
|
||||
"comment"
|
||||
]
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_sub_state_output_unifier_result_converted_to_true(self):
|
||||
"""
|
||||
Test for output is unified so the result is converted to True
|
||||
:return:
|
||||
"""
|
||||
data = {
|
||||
"sub_state_run": [
|
||||
{
|
||||
"comment": ["data", "in", "the", "list"],
|
||||
"changes": {},
|
||||
"name": None,
|
||||
"result": "Fantastic",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert (
|
||||
statedecorators.OutputUnifier("unify")(lambda: data)()["sub_state_run"][0][
|
||||
"result"
|
||||
]
|
||||
is True
|
||||
)
|
||||
|
||||
def test_sub_state_output_unifier_result_converted_to_false(self):
|
||||
"""
|
||||
Test for output is unified so the result is converted to False
|
||||
:return:
|
||||
"""
|
||||
data = {
|
||||
"sub_state_run": [
|
||||
{
|
||||
"comment": ["data", "in", "the", "list"],
|
||||
"changes": {},
|
||||
"name": None,
|
||||
"result": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert (
|
||||
statedecorators.OutputUnifier("unify")(lambda: data)()["sub_state_run"][0][
|
||||
"result"
|
||||
]
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
|
||||
"""
|
||||
TestCase for code handling slots
|
||||
|
|
Loading…
Add table
Reference in a new issue