File unify-logic-on-using-multiple-requisites-and-add-onf.patch of Package salt
From 3f8de377b860c7a65ca779123316377ade95f97b Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <vzhestkov@suse.com>
Date: Mon, 27 Jun 2022 17:53:54 +0300
Subject: [PATCH] Unify logic on using multiple requisites and add
onfail_all (bsc#1198738)
when https://github.com/saltstack/salt/issues/22370 was introduced it
actually removed functionality, as sometimes it desirable to onfail if
ALL of the listed requisites are failure. This adds back an 'onfail_all'
form that allows for this behavior (it would make more sense to have
onfail default to OR logic, but that would break backcompat, so we add
a new form instead).
Co-authored-by: Matt Phillips <mphillips81@bloomberg.net>
---
doc/ref/states/requisites.rst | 46 +++++++++++
salt/state.py | 18 +++-
.../files/file/base/requisites/onfail_all.sls | 54 ++++++++++++
.../requisites/onfail_multiple_required.sls | 20 +++++
tests/integration/modules/test_state.py | 82 +++++++++++++++++++
5 files changed, 216 insertions(+), 4 deletions(-)
create mode 100644 tests/integration/files/file/base/requisites/onfail_all.sls
diff --git a/doc/ref/states/requisites.rst b/doc/ref/states/requisites.rst
index f025302ace..c9b53e3125 100644
--- a/doc/ref/states/requisites.rst
+++ b/doc/ref/states/requisites.rst
@@ -232,6 +232,10 @@ There are several corresponding requisite_any statements:
* ``onchanges_any``
* ``onfail_any``
+Lastly, onfail has one special ``onfail_all`` form to account for when `AND`
+logic is desired instead of the default `OR` logic of onfail/onfail_any (which
+are equivalent).
+
All of the requisites define specific relationships and always work with the
dependency logic defined above.
@@ -521,6 +525,46 @@ The ``onfail`` requisite is applied in the same way as ``require`` as ``watch``:
- onfail:
- mount: primary_mount
+.. code-block:: yaml
+
+ build_site:
+ cmd.run:
+ - name: /srv/web/app/build_site
+
+ notify-build_failure:
+ hipchat.send_message:
+ - room_id: 123456
+ - message: "Building website fail on {{ salt.grains.get('id') }}"
+
+
+The default behavior of the ``onfail`` when multiple requisites are listed is
+the opposite of other requisites in the salt state engine, it acts by default
+like ``any()`` instead of ``all()``. This means that when you list multiple
+onfail requisites on a state, if *any* fail the requisite will be satisfied.
+If you instead need *all* logic to be applied, you can use ``onfail_all``
+form:
+
+.. code-block:: yaml
+
+ test_site_a:
+ cmd.run:
+ - name: ping -c1 10.0.0.1
+
+ test_site_b:
+ cmd.run:
+ - name: ping -c1 10.0.0.2
+
+ notify_site_down:
+ hipchat.send_message:
+ - room_id: 123456
+ - message: "Both primary and backup sites are down!"
+ - onfail_all:
+ - cmd: test_site_a
+ - cmd: test_site_b
+
+In this contrived example `notify_site_down` will run when both 10.0.0.1 and
+10.0.0.2 fail to respond to ping.
+
.. note::
Setting failhard (:ref:`globally <global-failhard>` or in
@@ -535,6 +579,8 @@ The ``onfail`` requisite is applied in the same way as ``require`` as ``watch``:
Beginning in the ``2016.11.0`` release of Salt, ``onfail`` uses OR logic for
multiple listed ``onfail`` requisites. Prior to the ``2016.11.0`` release,
``onfail`` used AND logic. See `Issue #22370`_ for more information.
+ Beginning in the ``Neon`` release of Salt, a new ``onfail_all`` requisite
+ form is available if AND logic is desired.
.. _Issue #22370: https://github.com/saltstack/salt/issues/22370
diff --git a/salt/state.py b/salt/state.py
index ddea13ad5e..af631c9573 100644
--- a/salt/state.py
+++ b/salt/state.py
@@ -74,6 +74,7 @@ STATE_REQUISITE_KEYWORDS = frozenset([
'onchanges_any',
'onfail',
'onfail_any',
+ 'onfail_all',
'onfail_stop',
'prereq',
'prerequired',
@@ -2496,6 +2497,8 @@ class State(object):
present = True
if 'onchanges' in low:
present = True
+ if 'onfail_all' in low:
+ present = True
if 'onchanges_any' in low:
present = True
if not present:
@@ -2509,6 +2512,7 @@ class State(object):
'prereq': [],
'onfail': [],
'onfail_any': [],
+ 'onfail_all': [],
'onchanges': [],
'onchanges_any': []}
if pre:
@@ -2597,7 +2601,7 @@ class State(object):
else:
if run_dict[tag].get('__state_ran__', True):
req_stats.add('met')
- if r_state.endswith('_any'):
+ if r_state.endswith('_any') or r_state == 'onfail':
if 'met' in req_stats or 'change' in req_stats:
if 'fail' in req_stats:
req_stats.remove('fail')
@@ -2607,8 +2611,14 @@ class State(object):
if 'fail' in req_stats:
req_stats.remove('fail')
if 'onfail' in req_stats:
- if 'fail' in req_stats:
+ # a met requisite in this case implies a success
+ if 'met' in req_stats:
req_stats.remove('onfail')
+ if r_state.endswith('_all'):
+ if 'onfail' in req_stats:
+ # a met requisite in this case implies a failure
+ if 'met' in req_stats:
+ req_stats.remove('met')
fun_stats.update(req_stats)
if 'unmet' in fun_stats:
@@ -2620,8 +2630,8 @@ class State(object):
status = 'met'
else:
status = 'pre'
- elif 'onfail' in fun_stats and 'met' not in fun_stats:
- status = 'onfail' # all onfail states are OK
+ elif 'onfail' in fun_stats and 'onchangesmet' not in fun_stats:
+ status = 'onfail'
elif 'onchanges' in fun_stats and 'onchangesmet' not in fun_stats:
status = 'onchanges'
elif 'change' in fun_stats:
diff --git a/tests/integration/files/file/base/requisites/onfail_all.sls b/tests/integration/files/file/base/requisites/onfail_all.sls
new file mode 100644
index 0000000000..c35838f076
--- /dev/null
+++ b/tests/integration/files/file/base/requisites/onfail_all.sls
@@ -0,0 +1,54 @@
+a:
+ cmd.run:
+ - name: exit 0
+
+b:
+ cmd.run:
+ - name: exit 0
+
+c:
+ cmd.run:
+ - name: exit 0
+
+d:
+ cmd.run:
+ - name: exit 1
+
+e:
+ cmd.run:
+ - name: exit 1
+
+f:
+ cmd.run:
+ - name: exit 1
+
+reqs not met:
+ cmd.run:
+ - name: echo itdidntonfail
+ - onfail_all:
+ - cmd: a
+ - cmd: e
+
+reqs also not met:
+ cmd.run:
+ - name: echo italsodidnonfail
+ - onfail_all:
+ - cmd: a
+ - cmd: b
+ - cmd: c
+
+reqs met:
+ cmd.run:
+ - name: echo itonfailed
+ - onfail_all:
+ - cmd: d
+ - cmd: e
+ - cmd: f
+
+reqs also met:
+ cmd.run:
+ - name: echo itonfailed
+ - onfail_all:
+ - cmd: d
+ - require:
+ - cmd: a
diff --git a/tests/integration/files/file/base/requisites/onfail_multiple_required.sls b/tests/integration/files/file/base/requisites/onfail_multiple_required.sls
index d4c49518e9..93c51cdfec 100644
--- a/tests/integration/files/file/base/requisites/onfail_multiple_required.sls
+++ b/tests/integration/files/file/base/requisites/onfail_multiple_required.sls
@@ -2,6 +2,10 @@ a:
cmd.run:
- name: exit 1
+pass:
+ cmd.run:
+ - name: exit 0
+
b:
cmd.run:
- name: echo b
@@ -23,3 +27,19 @@ d:
- cmd: a
- require:
- cmd: c
+
+e:
+ cmd.run:
+ - name: echo e
+ - onfail:
+ - cmd: pass
+ - require:
+ - cmd: c
+
+f:
+ cmd.run:
+ - name: echo f
+ - onfail:
+ - cmd: pass
+ - onchanges:
+ - cmd: b
diff --git a/tests/integration/modules/test_state.py b/tests/integration/modules/test_state.py
index d8ca033dc9..f6c6570acd 100644
--- a/tests/integration/modules/test_state.py
+++ b/tests/integration/modules/test_state.py
@@ -1033,6 +1033,81 @@ class StateModuleTest(ModuleCase, SaltReturnAssertsMixin):
self.assertReturnNonEmptySaltType(ret)
self.assertEqual(expected_result, result)
+ @skipIf(True, "SLOWTEST skip")
+ def test_requisites_onfail_all(self):
+ """
+ Call sls file containing several onfail-all
+
+ Ensure that some of them are failing and that the order is right.
+ """
+ expected_result = {
+ "cmd_|-a_|-exit 0_|-run": {
+ "__run_num__": 0,
+ "changes": True,
+ "comment": 'Command "exit 0" run',
+ "result": True,
+ },
+ "cmd_|-b_|-exit 0_|-run": {
+ "__run_num__": 1,
+ "changes": True,
+ "comment": 'Command "exit 0" run',
+ "result": True,
+ },
+ "cmd_|-c_|-exit 0_|-run": {
+ "__run_num__": 2,
+ "changes": True,
+ "comment": 'Command "exit 0" run',
+ "result": True,
+ },
+ "cmd_|-d_|-exit 1_|-run": {
+ "__run_num__": 3,
+ "changes": True,
+ "comment": 'Command "exit 1" run',
+ "result": False,
+ },
+ "cmd_|-e_|-exit 1_|-run": {
+ "__run_num__": 4,
+ "changes": True,
+ "comment": 'Command "exit 1" run',
+ "result": False,
+ },
+ "cmd_|-f_|-exit 1_|-run": {
+ "__run_num__": 5,
+ "changes": True,
+ "comment": 'Command "exit 1" run',
+ "result": False,
+ },
+ "cmd_|-reqs also met_|-echo itonfailed_|-run": {
+ "__run_num__": 9,
+ "changes": True,
+ "comment": 'Command "echo itonfailed" run',
+ "result": True,
+ },
+ "cmd_|-reqs also not met_|-echo italsodidnonfail_|-run": {
+ "__run_num__": 7,
+ "changes": False,
+ "comment": "State was not run because onfail req did not change",
+ "result": True,
+ },
+ "cmd_|-reqs met_|-echo itonfailed_|-run": {
+ "__run_num__": 8,
+ "changes": True,
+ "comment": 'Command "echo itonfailed" run',
+ "result": True,
+ },
+ "cmd_|-reqs not met_|-echo itdidntonfail_|-run": {
+ "__run_num__": 6,
+ "changes": False,
+ "comment": "State was not run because onfail req did not change",
+ "result": True,
+ },
+ }
+ ret = self.run_function("state.sls", mods="requisites.onfail_all")
+ result = self.normalize_ret(ret)
+ self.assertReturnNonEmptySaltType(ret)
+ self.assertEqual(expected_result, result)
+
+ @skipIf(True, "SLOWTEST skip")
def test_requisites_full_sls(self):
'''
Teste the sls special command in requisites
@@ -1718,6 +1793,13 @@ class StateModuleTest(ModuleCase, SaltReturnAssertsMixin):
stdout = state_run['cmd_|-d_|-echo d_|-run']['changes']['stdout']
self.assertEqual(stdout, 'd')
+ comment = state_run["cmd_|-e_|-echo e_|-run"]["comment"]
+ self.assertEqual(comment, "State was not run because onfail req did not change")
+
+ stdout = state_run["cmd_|-f_|-echo f_|-run"]["changes"]["stdout"]
+ self.assertEqual(stdout, "f")
+
+ @skipIf(True, "SLOWTEST skip")
def test_multiple_onfail_requisite_with_required_no_run(self):
'''
test to ensure multiple states are not run
--
2.36.1