1"""
2Utility functions for state functions
3
4.. versionadded:: 2018.3.0
5"""
6
7
8import copy
9
10import salt.state
11from salt.exceptions import CommandExecutionError
12
13_empty = object()
14
15
16def gen_tag(low):
17    """
18    Generate the running dict tag string from the low data structure
19    """
20    return "{0[state]}_|-{0[__id__]}_|-{0[name]}_|-{0[fun]}".format(low)
21
22
23def search_onfail_requisites(sid, highstate):
24    """
25    For a particular low chunk, search relevant onfail related states
26    """
27    onfails = []
28    if "_|-" in sid:
29        st = salt.state.split_low_tag(sid)
30    else:
31        st = {"__id__": sid}
32    for fstate, fchunks in highstate.items():
33        if fstate == st["__id__"]:
34            continue
35        else:
36            for mod_, fchunk in fchunks.items():
37                if not isinstance(mod_, str) or mod_.startswith("__"):
38                    continue
39                else:
40                    if not isinstance(fchunk, list):
41                        continue
42                    else:
43                        # bydefault onfail will fail, but you can
44                        # set onfail_stop: False to prevent the highstate
45                        # to stop if you handle it
46                        onfail_handled = False
47                        for fdata in fchunk:
48                            if not isinstance(fdata, dict):
49                                continue
50                            onfail_handled = fdata.get("onfail_stop", True) is False
51                            if onfail_handled:
52                                break
53                        if not onfail_handled:
54                            continue
55                        for fdata in fchunk:
56                            if not isinstance(fdata, dict):
57                                continue
58                            for knob, fvalue in fdata.items():
59                                if knob != "onfail":
60                                    continue
61                                for freqs in fvalue:
62                                    for fmod, fid in freqs.items():
63                                        if not (
64                                            fid == st["__id__"]
65                                            and fmod == st.get("state", fmod)
66                                        ):
67                                            continue
68                                        onfails.append((fstate, mod_, fchunk))
69    return onfails
70
71
72def check_onfail_requisites(state_id, state_result, running, highstate):
73    """
74    When a state fail and is part of a highstate, check
75    if there is onfail requisites.
76    When we find onfail requisites, we will consider the state failed
77    only if at least one of those onfail requisites also failed
78
79    Returns:
80
81        True: if onfail handlers succeeded
82        False: if one on those handler failed
83        None: if the state does not have onfail requisites
84
85    """
86    nret = None
87    if state_id and state_result and highstate and isinstance(highstate, dict):
88        onfails = search_onfail_requisites(state_id, highstate)
89        if onfails:
90            for handler in onfails:
91                fstate, mod_, fchunk = handler
92                for rstateid, rstate in running.items():
93                    if "_|-" in rstateid:
94                        st = salt.state.split_low_tag(rstateid)
95                    # in case of simple state, try to guess
96                    else:
97                        id_ = rstate.get("__id__", rstateid)
98                        if not id_:
99                            raise ValueError("no state id")
100                        st = {"__id__": id_, "state": mod_}
101                    if mod_ == st["state"] and fstate == st["__id__"]:
102                        ofresult = rstate.get("result", _empty)
103                        if ofresult in [False, True]:
104                            nret = ofresult
105                        if ofresult is False:
106                            # as soon as we find an errored onfail, we stop
107                            break
108                # consider that if we parsed onfailes without changing
109                # the ret, that we have failed
110                if nret is None:
111                    nret = False
112    return nret
113
114
115def check_result(running, recurse=False, highstate=None):
116    """
117    Check the total return value of the run and determine if the running
118    dict has any issues
119    """
120    if not isinstance(running, dict):
121        return False
122
123    if not running:
124        return False
125
126    ret = True
127    for state_id, state_result in running.items():
128        expected_type = dict
129        # The __extend__ state is a list
130        if "__extend__" == state_id:
131            expected_type = list
132        if not recurse and not isinstance(state_result, expected_type):
133            ret = False
134        if ret and isinstance(state_result, dict):
135            result = state_result.get("result", _empty)
136            if result is False:
137                ret = False
138            # only override return value if we are not already failed
139            elif result is _empty and isinstance(state_result, dict) and ret:
140                ret = check_result(state_result, recurse=True, highstate=highstate)
141        # if we detect a fail, check for onfail requisites
142        if not ret:
143            # ret can be None in case of no onfail reqs, recast it to bool
144            ret = bool(
145                check_onfail_requisites(state_id, state_result, running, highstate)
146            )
147        # return as soon as we got a failure
148        if not ret:
149            break
150    return ret
151
152
153def merge_subreturn(original_return, sub_return, subkey=None):
154    """
155    Update an existing state return (`original_return`) in place
156    with another state return (`sub_return`), i.e. for a subresource.
157
158    Returns:
159        dict: The updated state return.
160
161    The existing state return does not need to have all the required fields,
162    as this is meant to be called from the internals of a state function,
163    but any existing data will be kept and respected.
164
165    It is important after using this function to check the return value
166    to see if it is False, in which case the main state should return.
167    Prefer to check `_ret['result']` instead of `ret['result']`,
168    as the latter field may not yet be populated.
169
170    Code Example:
171
172    .. code-block:: python
173
174        def state_func(name, config, alarm=None):
175            ret = {'name': name, 'comment': '', 'changes': {}}
176            if alarm:
177                _ret = __states__['subresource.managed'](alarm)
178                __utils__['state.merge_subreturn'](ret, _ret)
179                if _ret['result'] is False:
180                    return ret
181    """
182    if not subkey:
183        subkey = sub_return["name"]
184
185    if sub_return["result"] is False:
186        # True or None stay the same
187        original_return["result"] = sub_return["result"]
188
189    sub_comment = sub_return["comment"]
190    if not isinstance(sub_comment, list):
191        sub_comment = [sub_comment]
192    original_return.setdefault("comment", [])
193    if isinstance(original_return["comment"], list):
194        original_return["comment"].extend(sub_comment)
195    else:
196        if original_return["comment"]:
197            # Skip for empty original comments
198            original_return["comment"] += "\n"
199        original_return["comment"] += "\n".join(sub_comment)
200
201    if sub_return["changes"]:  # changes always exists
202        original_return.setdefault("changes", {})
203        original_return["changes"][subkey] = sub_return["changes"]
204
205    return original_return
206
207
208def get_sls_opts(opts, **kwargs):
209    """
210    Return a copy of the opts for use, optionally load a local config on top
211    """
212    opts = copy.deepcopy(opts)
213
214    if "localconfig" in kwargs:
215        return salt.config.minion_config(kwargs["localconfig"], defaults=opts)
216
217    if "saltenv" in kwargs:
218        saltenv = kwargs["saltenv"]
219        if saltenv is not None:
220            if not isinstance(saltenv, str):
221                saltenv = str(saltenv)
222            if opts["lock_saltenv"] and saltenv != opts["saltenv"]:
223                raise CommandExecutionError(
224                    "lock_saltenv is enabled, saltenv cannot be changed"
225                )
226            opts["saltenv"] = kwargs["saltenv"]
227
228    if "pillarenv" in kwargs or opts.get("pillarenv_from_saltenv", False):
229        pillarenv = kwargs.get("pillarenv") or kwargs.get("saltenv")
230        if pillarenv is not None and not isinstance(pillarenv, str):
231            opts["pillarenv"] = str(pillarenv)
232        else:
233            opts["pillarenv"] = pillarenv
234
235    return opts
236