fixes saltstack/salt#62446 add global_state_conditions handling

This commit is contained in:
nicholasmhughes 2022-09-20 09:21:14 -04:00 committed by Twangboy
parent 93f871df58
commit d258b87a08
No known key found for this signature in database
GPG key ID: ED267D5C0DE6F8A6
6 changed files with 132 additions and 3 deletions

1
changelog/62446.added Normal file
View file

@ -0,0 +1 @@
Add ability to provide conditions which convert normal state actions to no-op when true

View file

@ -600,6 +600,14 @@
# - require
# - require_in
# If set, this parameter expects a dictionary of state module names as keys
# and list of conditions which must be satisfied in order to run any functions
# in that state module.
#
#global_state_conditions:
# "*": ["G@global_noop:false"]
# service: ["not G@virtual_subtype:chroot"]
##### File Directory Settings #####
##########################################
# The Salt Minion can redirect all file server operations to a local directory,

View file

@ -2472,6 +2472,21 @@ default configuration set up at install time.
snapper_states_config: root
``global_state_conditions``
-------------------------
Default: ``None``
If set, this parameter expects a dictionary of state module names as keys and
list of conditions which must be satisfied in order to run any functions in that
state module.
.. code-block:: yaml
global_state_conditions:
"*": ["G@global_noop:false"]
service: ["not G@virtual_subtype:chroot"]
File Directory Settings
=======================

View file

@ -955,6 +955,7 @@ VALID_OPTS = immutabletypes.freeze(
# client via the Salt API
"netapi_allow_raw_shell": bool,
"disabled_requisites": (str, list),
"global_state_conditions": dict,
# Feature flag config
"features": dict,
"fips_mode": bool,
@ -1273,6 +1274,7 @@ DEFAULT_MINION_OPTS = immutabletypes.freeze(
"schedule": {},
"ssh_merge_pillar": True,
"disabled_requisites": [],
"global_state_conditions": None,
"reactor_niceness": None,
"fips_mode": False,
}

View file

@ -787,6 +787,36 @@ class State:
self.inject_globals = {}
self.mocked = mocked
def _match_global_state_conditions(self, full, state, name):
"""
Return ``None`` if global state conditions are met. Otherwise, pass a
return dictionary which effectively creates a no-op outcome.
This operation is "explicit allow", in that ANY state and condition
combination which matches will allow the state to be run.
"""
matches = []
ret = None
ret_dict = {
"name": name,
"comment": "Failed to meet global state conditions. State not called.",
"changes": {},
"result": None,
}
if isinstance(self.opts.get("global_state_conditions"), dict):
for state_match, conditions in self.opts["global_state_conditions"].items():
if state_match in ["*", full, state]:
if isinstance(conditions, str):
conditions = [conditions]
if isinstance(conditions, list):
matches.extend(
self.functions["match.compound"](condition)
for condition in conditions
)
if matches and not any(matches):
ret = ret_dict
return ret
def _gather_pillar(self):
"""
Whenever a state run starts, gather the pillar data fresh
@ -2046,7 +2076,11 @@ class State:
instance.format_slots(cdata)
tag = _gen_tag(low)
try:
ret = instance.states[cdata["full"]](*cdata["args"], **cdata["kwargs"])
ret = instance._match_global_state_conditions(
cdata["full"], low["state"], name
)
if not ret:
ret = instance.states[cdata["full"]](*cdata["args"], **cdata["kwargs"])
except Exception as exc: # pylint: disable=broad-except
log.debug(
"An exception occurred in this state: %s",
@ -2305,9 +2339,13 @@ class State:
ret = self.call_parallel(cdata, low)
else:
self.format_slots(cdata)
ret = self.states[cdata["full"]](
*cdata["args"], **cdata["kwargs"]
ret = self._match_global_state_conditions(
cdata["full"], low["state"], low["name"]
)
if not ret:
ret = self.states[cdata["full"]](
*cdata["args"], **cdata["kwargs"]
)
self.states.inject_globals = {}
if (
"check_cmd" in low

View file

@ -0,0 +1,65 @@
import logging
import pytest
import salt.config
import salt.state
log = logging.getLogger(__name__)
@pytest.fixture
def minion_config():
cfg = salt.config.DEFAULT_MINION_OPTS.copy()
cfg["file_client"] = "local"
cfg["id"] = "foo01"
return cfg
def test_global_state_conditions_unconfigured(minion_config):
state_obj = salt.state.State(minion_config)
ret = state_obj._match_global_state_conditions(
"test.succeed_with_changes", "test", "mytest"
)
assert ret is None
@pytest.mark.parametrize("condition", [["foo01"], "foo01"])
def test_global_state_conditions_match(minion_config, condition):
minion_config["global_state_conditions"] = {
"test": condition,
}
state_obj = salt.state.State(minion_config)
ret = state_obj._match_global_state_conditions(
"test.succeed_with_changes", "test", "mytest"
)
assert ret is None
def test_global_state_conditions_no_match(minion_config):
minion_config["global_state_conditions"] = {
"test.succeed_with_changes": ["bar01"],
}
state_obj = salt.state.State(minion_config)
ret = state_obj._match_global_state_conditions(
"test.succeed_with_changes", "test", "mytest"
)
assert ret == {
"changes": {},
"comment": "Failed to meet global state conditions. State not called.",
"name": "mytest",
"result": None,
}
def test_global_state_conditions_match_one_of_many(minion_config):
minion_config["global_state_conditions"] = {
"test.succeed_with_changes": ["bar01"],
"test": ["baz01"],
"*": ["foo01"],
}
state_obj = salt.state.State(minion_config)
ret = state_obj._match_global_state_conditions(
"test.succeed_with_changes", "test", "mytest"
)
assert ret is None