From d5183ffd2bbfcc91c95e5d134c44c88ad0f9f8da Mon Sep 17 00:00:00 2001 From: bdrx312 Date: Mon, 3 Nov 2025 20:36:09 -0500 Subject: [PATCH 1/3] Add capability to specify state args as a dict Also add some more type hints and change some format calls to f strings. Fix state retry tests with invalid assumptions of duration and comment. --- changelog/68367.added.md | 1 + salt/state.py | 498 +++++++++--------- .../functional/modules/state/test_state.py | 110 ++-- .../pytests/integration/cli/test_salt_call.py | 22 + .../pytests/integration/modules/test_state.py | 8 +- tests/pytests/integration/states/test_file.py | 4 +- .../pytests/pkg/integration/test_salt_call.py | 39 +- .../unit/state/test_reactor_compiler.py | 2 +- tests/pytests/unit/state/test_state_basic.py | 53 +- .../pytests/unit/state/test_state_compiler.py | 33 +- 10 files changed, 464 insertions(+), 306 deletions(-) create mode 100644 changelog/68367.added.md diff --git a/changelog/68367.added.md b/changelog/68367.added.md new file mode 100644 index 000000000000..e6331b0432d1 --- /dev/null +++ b/changelog/68367.added.md @@ -0,0 +1 @@ +Added capability to specify module arguments in an sls as a dict diff --git a/salt/state.py b/salt/state.py index b3432215eb5f..bc4a222a1af8 100644 --- a/salt/state.py +++ b/salt/state.py @@ -26,8 +26,8 @@ import site import time import traceback -from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence -from typing import Any, Union +from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence +from typing import Any, TypeVar, Union import networkx as nx @@ -72,9 +72,13 @@ HighData = dict[str, Union[Mapping[str, Any], list[Union[Mapping[str, Any], str]]]] LowChunk = dict[str, Any] +K = TypeVar("K") +V = TypeVar("V") +Pair = tuple[K, V] +HighDataStateArgsDef = Union[Mapping[K, V], Iterable[Union[Mapping[K, V], str]]] # These are keywords passed to state module functions which are to be used -# by salt in this state module and not on the actual state module function +# by salt in this state module and not by the actual state module function STATE_REQUISITE_KEYWORDS = frozenset( [req_type.value for req_type in RequisiteType] + [ @@ -97,8 +101,6 @@ "retry", "order", "parallel", - "prereq", - "prereq_in", "reload_modules", "reload_grains", "reload_pillar", @@ -127,7 +129,6 @@ "__umask__", ] ) - STATE_INTERNAL_KEYWORDS = STATE_REQUISITE_KEYWORDS.union( STATE_REQUISITE_IN_KEYWORDS ).union(STATE_RUNTIME_KEYWORDS) @@ -189,21 +190,19 @@ def get_accumulator_dir(cachedir): return fn_ -def state_args(id_: Hashable, state: Hashable, high: HighData) -> set[Any]: +def state_args(id_: str, state: str, high: HighData) -> set[Any]: """ - Return a set of the arguments passed to the named state + Return a set of the argument names passed to the named state """ args = set() if id_ not in high: return args - if state not in high[id_]: + body = high[id_] + if not isinstance(body, Mapping) or state not in body: return args - for item in high[id_][state]: - if not isinstance(item, dict): - continue - if len(item) != 1: - continue - args.add(next(iter(item))) + for key, _ in _state_args_kv_iter(body[state], id_): + if key != "fun": + args.add(key) return args @@ -251,7 +250,7 @@ def find_sls_ids(sls: Any, high: HighData) -> list[tuple[str, str]]: ret = [] for nid, item in high.items(): try: - sls_tgt = item["__sls__"] + sls_tgt = item["__sls__"] # type: ignore (except case handles invalid type) except TypeError: if nid != "__exclude__": log.error( @@ -281,11 +280,11 @@ def format_log(ret: Any) -> None: if ret["comment"]: msg = ret["comment"] else: - msg = "No changes made for {0[name]}".format(ret) + msg = f"No changes made for {ret['name']}" elif isinstance(chg, dict): if "diff" in chg: if isinstance(chg["diff"], str): - msg = "File changed:\n{}".format(chg["diff"]) + msg = f"File changed:\n{chg['diff']}" if all([isinstance(x, dict) for x in chg.values()]): if all([("old" in x and "new" in x) for x in chg.values()]): msg = "Made the following changes:\n" @@ -298,9 +297,7 @@ def format_log(ret: Any) -> None: new = "absent" # This must be able to handle unicode as some package names contain # non-ascii characters like "Français" or "Español". See Issue #33605. - msg += "'{}' changed from '{}' to '{}'\n".format( - pkg, old, new - ) + msg += f"'{pkg}' changed from '{old}' to '{new}'\n" if not msg: msg = str(ret["changes"]) if ret["result"] is True or ret["result"] is None: @@ -388,6 +385,37 @@ def _apply_exclude(high: HighData) -> HighData: return high +def _state_args_kv_iter( + obj: HighDataStateArgsDef, + id_: Hashable, +) -> Iterator[Pair]: + """ + Yield (key, value) pairs from either: + - Mapping[K, V] -> yields its .items() + - Iterable of elements where each element is either: + * Mapping[K, V] + * str -> yields Mapping["fun", V] + """ + if isinstance(obj, Mapping): + yield from obj.items() + return + + if not isinstance(obj, Iterable): + raise TypeError( + f"expected a mapping or a list of str/mapping for state arguments of state {id_} instead of {type(obj)!r} {obj}" + ) + + for i, item in enumerate(obj): + if isinstance(item, Mapping): + yield from item.items() + elif isinstance(item, str): + yield ("fun", item) + else: + raise TypeError( + f"unsupported element type {type(item)!r} at index {i} of {obj} of state {id_}" + ) + + def _verify_high(high: dict) -> list[str]: """ Verify that the high data is viable and follows the data structure @@ -407,27 +435,34 @@ def _verify_high(high: dict) -> list[str]: err = f"The type {id_} in {body} is not formatted as a dictionary" errors.append(err) continue - for state in body: + for state, args in body.items(): if state.startswith("__"): continue - if body[state] is None: + if args is None: errors.append( f"ID '{id_}' in SLS '{body['__sls__']}' contains a short declaration " f"({state}) with a trailing colon. When not passing any " "arguments to a state, the colon must be omitted." ) continue - if not isinstance(body[state], list): + if not isinstance(args, list) and not isinstance(args, dict): errors.append( - f"State '{id_}' in SLS '{body['__sls__']}' is not formed as a list" + f"State '{id_}' in SLS '{body['__sls__']}' is not formed as a list or dict" ) + continue + fun_count = 0 + if "." in state: + # This should not happen usually since `_handle_state_decls` or + # `pad_funcs` is run on rendered templates + fun_count += 1 + if isinstance(args, dict): + for key, value in args.items(): + if key == "fun": + fun_count += 1 + else: + _verify_high_state_key_value_arg(id_, key, value, body, errors) else: - fun_count = 0 - if "." in state: - # This should not happen usually since `_handle_state_decls` or - # `pad_funcs` is run on rendered templates - fun_count += 1 - for arg in body[state]: + for arg in args: if isinstance(arg, str): fun_count += 1 if " " in arg.strip(): @@ -440,75 +475,76 @@ def _verify_high(high: dict) -> list[str]: ) elif isinstance(arg, dict): - # The arg is a dict, if the arg is require or - # watch, it must be a list. - argfirst = next(iter(arg)) - if argfirst == "names": - if not isinstance(arg[argfirst], list): - errors.append( - "The 'names' argument in state " - f"'{id_}' in SLS '{body['__sls__']}' needs to be " - "formed as a list" - ) - if argfirst in STATE_REQUISITE_KEYWORDS: - if not isinstance(arg[argfirst], list): - errors.append( - f"The {argfirst} statement in state '{id_}' in " - f"SLS '{body['__sls__']}' needs to be formed as a " - "list" - ) - # It is a list, verify that the members of the - # list are all single key dicts. - else: - for req in arg[argfirst]: - if isinstance(req, str): - req = {"id": req} - if not isinstance(req, dict) or len(req) != 1: - errors.append( - f"Requisite declaration {req} in " - f"state {id_} in SLS {body['__sls__']} " - "is not formed as a single key dictionary" - ) - continue - # req_key: the name or id of the required state; the requisite will match both - # req_val: the type of requirement i.e. id, sls, name of state module like file - req_key, req_val = next(iter(req.items())) - if "." in req_key: - errors.append( - f"Invalid requisite type '{req_key}' " - f"in state '{id_}', in SLS " - f"'{ body['__sls__']}'. Requisite types must " - "not contain dots, did you " - f"mean '{req_key[: req_key.find('.')]}'?" - ) - if not ishashable(req_val): - errors.append( - f'Illegal requisite "{req_val}" ' - f'in SLS "{body["__sls__"]}", ' - "please check your syntax.\n" - ) - continue - # Make sure that there is only one key in the - # dict - if len(list(arg)) != 1: - errors.append( - "Multiple dictionaries defined in " - f"argument of state '{id_}' in SLS '{body['__sls__']}'" - ) - if not fun_count: + first_key, first_value = next(iter(arg.items())) + _verify_high_state_key_value_arg( + id_, first_key, first_value, body, errors + ) + if not fun_count: + errors.append( + f"No function declared in state '{id_}' in SLS " + f"'{body['__sls__']}'" + ) + elif fun_count > 1: + funs = [state.split(".", maxsplit=1)[1]] if "." in state else [] + funs.extend(arg for arg in body[state] if isinstance(arg, str)) + errors.append( + f"Too many functions declared in state '{id_}' in " + f"SLS '{body['__sls__']}'. Please choose one of " + "the following: " + ", ".join(funs) + ) + return errors + + +def _verify_high_state_key_value_arg( + id_: str, arg_name: str, value, body: dict, errors: list[str] +): + if arg_name == "names": + if not isinstance(value, list): + errors.append( + "The 'names' argument in state " + f"'{id_}' in SLS '{body['__sls__']}' needs to be " + "formed as a list" + ) + elif arg_name in STATE_REQUISITE_KEYWORDS: + # if the arg is a requisite, + # it must be a list. + if not isinstance(value, list): + errors.append( + f"The {arg_name} statement in state '{id_}' in " + f"SLS '{body['__sls__']}' needs to be formed as a " + "list" + ) + # It is a list, verify that the members of the + # list are all single key dicts. + else: + for req in value: + if isinstance(req, str): + req = {"id": req} + if not isinstance(req, dict) or len(req) != 1: errors.append( - f"No function declared in state '{id_}' in SLS " - f"'{body['__sls__']}'" + f"Requisite declaration {req} in " + f"state {id_} in SLS {body['__sls__']} " + "is not formed as a single key dictionary" ) - elif fun_count > 1: - funs = [state.split(".", maxsplit=1)[1]] if "." in state else [] - funs.extend(arg for arg in body[state] if isinstance(arg, str)) + continue + # req_key: the name or id of the required state; the requisite will match both + # req_val: the type of requirement i.e. id, sls, name of state module like file + req_key, req_val = next(iter(req.items())) + if "." in req_key: errors.append( - f"Too many functions declared in state '{id_}' in " - f"SLS '{body['__sls__']}'. Please choose one of " - "the following: " + ", ".join(funs) + f"Invalid requisite type '{req_key}' " + f"in state '{id_}', in SLS " + f"'{ body['__sls__']}'. Requisite types must " + "not contain dots, did you " + f"mean '{req_key[: req_key.find('.')]}'?" ) - return errors + if not ishashable(req_val): + errors.append( + f'Illegal requisite "{req_val}" ' + f'in SLS "{body["__sls__"]}", ' + "please check your syntax.\n" + ) + continue class StateError(Exception): @@ -547,33 +583,27 @@ def pad_funcs(self, high): """ Turns dot delimited function refs into function strings """ - for name in high: - if not isinstance(high[name], dict): - if isinstance(high[name], str): + for id_, body in high.items(): + if not isinstance(body, dict): + if isinstance(body, str): # Is this is a short state? It needs to be padded! - if "." in high[name]: - comps = high[name].split(".") - if len(comps) >= 2: - # Merge the comps - comps[1] = ".".join(comps[1 : len(comps)]) - high[name] = { + if "." in body: + state, function_name = body.split(".", maxsplit=1) + high[id_] = { # '__sls__': template, # '__env__': None, - comps[0]: [comps[1]] + state: [function_name] } continue continue - skeys = set() - for key in sorted(high[name]): + state_keys = set() + for key, value in sorted(body.items()): if key.startswith("_"): continue - if not isinstance(high[name][key], list): + if not isinstance(value, (list, dict)): continue if "." in key: - comps = key.split(".") - if len(comps) >= 2: - # Merge the comps - comps[1] = ".".join(comps[1 : len(comps)]) + state, function_name = key.split(".", maxsplit=1) # Salt doesn't support state files such as: # # /etc/redis/redis.conf: @@ -583,13 +613,16 @@ def pad_funcs(self, high): # - mode: 644 # file.comment: # - regex: ^requirepass - if comps[0] in skeys: + if state in state_keys: continue - high[name][comps[0]] = high[name].pop(key) - high[name][comps[0]].append(comps[1]) - skeys.add(comps[0]) + high[id_][state] = high[id_].pop(key) + if isinstance(value, dict): + high[id_][state][function_name] = None + else: + high[id_][state].append(function_name) + state_keys.add(state) continue - skeys.add(key) + state_keys.add(key) return high def verify_high(self, high): @@ -892,12 +925,10 @@ def _mod_init(self, low): """ # ensure that the module is loaded try: - self.states[ - "{}.{}".format(low["state"], low["fun"]) - ] # pylint: disable=W0106 + self.states[f"{low['state']}.{low['fun']}"] except KeyError: return - minit = "{}.mod_init".format(low["state"]) + minit = f"{low['state']}.mod_init" if low["state"] not in self.mod_init: if minit in self.states._dict: mret = self.states[minit](low) @@ -1192,7 +1223,7 @@ def _run_check_creates(self, low: LowChunk) -> dict[str, Any]: ret[key] = low.get(key) if isinstance(low["creates"], str) and os.path.exists(low["creates"]): - ret["comment"] = "{} exists".format(low["creates"]) + ret["comment"] = f"{low['creates']} exists" ret["result"] = True ret["skip_watch"] = True elif isinstance(low["creates"], list) and all( @@ -1260,7 +1291,7 @@ def load_modules(self, data=None, proxy=None): ) if funcs: for func in funcs: - f_key = "{}{}".format(mod, func[func.rindex(".") :]) + f_key = f"{mod}{func[func.rindex('.') :]}" self.functions[f_key] = funcs[func] self.serializers = salt.loader.serializers(self.opts) self._load_states() @@ -1368,7 +1399,7 @@ def verify_data(self, data: dict[str, Any]) -> list[str]: if full not in self.states: if "__sls__" in data: errors.append( - "State '{}' was not found in SLS '{}'".format(full, data["__sls__"]) + f"State '{full}' was not found in SLS '{data['__sls__']}'" ) reason = self.states.missing_fun_string(full) if reason: @@ -1387,9 +1418,7 @@ def verify_data(self, data: dict[str, Any]) -> list[str]: for ind in range(arglen - deflen): if aspec.args[ind] not in data: errors.append( - "Missing parameter {} for state {}".format( - aspec.args[ind], full - ) + f"Missing parameter {aspec.args[ind]} for state {full}" ) return errors @@ -1475,7 +1504,7 @@ def compile_high_data( for id_, body in high.items(): if id_.startswith("__"): continue - for state, run in body.items(): + for state, args in body.items(): # This should be a single value instead of a set # because multiple functions of the same state # type are not allowed in the same state @@ -1493,24 +1522,21 @@ def compile_high_data( if "__sls_included_from__" in body: chunk["__sls_included_from__"] = body["__sls_included_from__"] chunk["__id__"] = id_ - for arg in run: - if isinstance(arg, str): - funcs.add(arg) + for key, val in _state_args_kv_iter(args, id_): + if key == "fun": + funcs.add(val) + elif key == "names": + for _name in val: + if _name not in names: + names.append(_name) + elif key == "state": + # Don't pass down a state override continue - if isinstance(arg, dict): - for key, val in arg.items(): - if key == "names": - for _name in val: - if _name not in names: - names.append(_name) - elif key == "state": - # Don't pass down a state override - continue - elif key == "name" and not isinstance(val, str): - # Invalid name, fall back to ID - chunk[key] = id_ - else: - chunk[key] = val + elif key == "name" and not isinstance(val, str): + # Invalid name, fall back to ID + chunk[key] = id_ + else: + chunk[key] = val if names: name_order = 1 for entry in names: @@ -1593,7 +1619,7 @@ def reconcile_extend(self, high: HighData, strict=False): errors = [] if "__extend__" not in high: return high, errors - ext = high.pop("__extend__") + ext: list[Mapping[str, Any]] = high.pop("__extend__") for ext_chunk in ext: for name, body in ext_chunk.items(): state_type = next(x for x in body if not x.startswith("__")) @@ -1746,15 +1772,9 @@ def requisite_in(self, high: HighData): extend[name] = HashableOrderedDict() if "." in _state: errors.append( - "Invalid requisite in {}: {} for " - "{} in SLS '{}'. Requisites must " - "not contain dots, did you mean '{}'?".format( - rkey, - _state, - name, - body["__sls__"], - _state[: _state.find(".")], - ) + f"Invalid requisite in {rkey}: {_state} for " + f"{name} in SLS '{body['__sls__']}'. Requisites must " + f"not contain dots, did you mean '{_state[: _state.find('.')]}'?" ) _state = _state.split(".")[0] if _state not in extend[name]: @@ -1983,10 +2003,8 @@ def _call_parallel_target( _comment = "" else: _comment = ( - 'Attempt {}: Returned a result of "{}", ' - 'with the following comment: "{}"'.format( - retries, ret["result"], ret["comment"] - ) + f'Attempt {retries}: Returned a result of "{ret["result"]}", ' + f'with the following comment: "{ret["comment"]}"' ) ret["comment"] = "\n".join([orig_ret["comment"], _comment]) @@ -2141,7 +2159,7 @@ def call( if "provider" in low: self.load_modules(low) - state_func_name = "{0[state]}.{0[fun]}".format(low) + state_func_name = f"{low['state']}.{low['fun']}" cdata = salt.utils.args.format_call( self.states[state_func_name], low, @@ -2182,7 +2200,7 @@ def call( req_list = ("unless", "onlyif", "creates") if ( any(req in low for req in req_list) - and "{0[state]}.mod_run_check".format(low) not in self.states + and f"{low['state']}.mod_run_check" not in self.states ): ret.update(self._run_check(low)) @@ -2233,8 +2251,8 @@ def call( ) self.states.inject_globals = {} if "check_cmd" in low: - state_check_cmd = "{0[state]}.mod_run_check_cmd".format(low) - state_func = "{0[state]}.{0[fun]}".format(low) + state_check_cmd = f"{low['state']}.mod_run_check_cmd" + state_func = f"{low['state']}.{low['fun']}" state_func_sig = inspect.signature(self.states[state_func]) if state_check_cmd not in self.states: ret.update(self._run_check_cmd(low)) @@ -2325,10 +2343,8 @@ def call( ret = retry_ret ret["comment"] = "\n".join( [ - 'Attempt {}: Returned a result of "{}", ' - 'with the following comment: "{}"'.format( - retries, orig_ret["result"], orig_ret["comment"] - ), + f'Attempt {retries}: Returned a result of "{orig_ret["result"]}", ' + f'with the following comment: "{orig_ret["comment"]}"', "" if not ret["comment"] else ret["comment"], ] ) @@ -2934,9 +2950,7 @@ def call_chunk( # If the result was False (not None) it was a failure if req_ret["result"] is False: # use SLS.ID for the key-- so its easier to find - key = "{sls}.{_id}".format( - sls=req_low["__sls__"], _id=req_low["__id__"] - ) + key = f"{req_low['__sls__']}.{req_low['__id__']}" failed_requisites.add(key) _cmt = "One or more requisite failed: {}".format( @@ -3140,9 +3154,7 @@ def call_listen(self, chunks: Iterable[LowChunk], running: dict) -> dict: rerror = { _l_tag(lkey, lval): { "comment": ( - "Referenced state {}: {} does not exist".format( - lkey, lval - ) + f"Referenced state {lkey}: {lval} does not exist" ), "name": f"listen_{lkey}:{lval}", "result": False, @@ -3167,13 +3179,9 @@ def call_listen(self, chunks: Iterable[LowChunk], running: dict) -> dict: rerror = { _l_tag(key[0], key[1]): { "comment": ( - "Referenced state {}: {} does not exist".format( - key[0], key[1] - ) - ), - "name": "listen_{}:{}".format( - key[0], key[1] + f"Referenced state {key[0]}: {key[1]} does not exist" ), + "name": f"listen_{key[0]}:{key[1]}", "result": False, "changes": {}, } @@ -3190,7 +3198,7 @@ def call_listen(self, chunks: Iterable[LowChunk], running: dict) -> dict: low = chunk.copy() low["sfun"] = chunk["fun"] low["fun"] = "mod_watch" - low["__id__"] = "listener_{}".format(low["__id__"]) + low["__id__"] = f"listener_{low['__id__']}" for req in STATE_REQUISITE_KEYWORDS: if req in low: low.pop(req) @@ -3231,7 +3239,6 @@ def call_high( return errors # If there are extensions in the highstate, process them and update # the low data chunks - ret = self.call_chunks(chunks, disabled_states=self.disabled_states) ret = self.call_listen(chunks, ret) ret = self.call_beacons(chunks, ret) @@ -3271,8 +3278,8 @@ def render_template(self, high, template): for item in invalid_items: if item in high: errors.append( - "The '{}' declaration found on '{}' is invalid when " - "rendering single templates".format(item, template) + f"The '{item}' declaration found on '{template}' is invalid when " + "rendering single templates" ) return high, errors @@ -3290,9 +3297,7 @@ def render_template(self, high, template): continue errors.append( - "ID {} in template {} is not a dictionary".format( - name, template - ) + f"ID {name} in template {template} is not a dictionary" ) continue skeys = set() @@ -3301,10 +3306,10 @@ def render_template(self, high, template): continue if high[name][key] is None: errors.append( - "ID '{}' in template {} contains a short " - "declaration ({}) with a trailing colon. When not " + f"ID '{name}' in template {template} contains a short " + f"declaration ({key}) with a trailing colon. When not " "passing any arguments to a state, the colon must be " - "omitted.".format(name, template, key) + "omitted." ) continue if not isinstance(high[name][key], list): @@ -3322,8 +3327,8 @@ def render_template(self, high, template): # - regex: ^requirepass if comps[0] in skeys: errors.append( - "ID '{}' in template '{}' contains multiple " - "state declarations of the same type".format(name, template) + f"ID '{name}' in template '{template}' contains multiple " + "state declarations of the same type" ) continue high[name][comps[0]] = high[name].pop(key) @@ -3851,16 +3856,14 @@ def verify_tops(self, tops): continue if not isinstance(saltenv, str): errors.append( - "Environment {} in top file is not formed as a string".format( - saltenv - ) + f"Environment {saltenv} in top file is not formed as a string" ) if saltenv == "": errors.append("Empty saltenv statement in top file") if not isinstance(matches, dict): errors.append( - "The top file matches for saltenv {} are not " - "formatted as a dict".format(saltenv) + f"The top file matches for saltenv {saltenv} are not " + "formatted as a dict" ) for slsmods in matches.values(): if not isinstance(slsmods, list): @@ -3875,15 +3878,13 @@ def verify_tops(self, tops): if not val: errors.append( "Improperly formatted top file matcher " - "in saltenv {}: {} file".format(slsmod, val) + f"in saltenv {slsmod}: {val} file" ) elif isinstance(slsmod, str): # This is a sls module if not slsmod: errors.append( - "Environment {} contains an empty sls index".format( - saltenv - ) + f"Environment {saltenv} contains an empty sls index" ) return errors @@ -4023,9 +4024,9 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): state = None if not fn_: errors.append( - "Specified SLS {} in saltenv {} is not " + f"Specified SLS {sls} in saltenv {saltenv} is not " "available on the salt master or through a configured " - "fileserver".format(sls, saltenv) + "fileserver" ) else: try: @@ -4065,8 +4066,8 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): if "include" in state: if not isinstance(state["include"], list): err = ( - "Include Declaration in SLS {} is not formed " - "as a list".format(sls) + f"Include Declaration in SLS {sls} is not formed " + "as a list" ) errors.append(err) else: @@ -4090,10 +4091,8 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): if env_key not in self.avail and "__env__" not in self.avail: msg = ( - "Nonexistent saltenv '{}' found in include " - "of '{}' within SLS '{}:{}'".format( - env_key, inc_sls, saltenv, sls - ) + f"Nonexistent saltenv '{env_key}' found in include " + f"of '{inc_sls}' within SLS '{saltenv}:{sls}'" ) log.error(msg) errors.append(msg) @@ -4105,8 +4104,8 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): levels, include = match.groups() else: msg = ( - "Badly formatted include {} found in include " - "in SLS '{}:{}'".format(inc_sls, saltenv, sls) + f"Badly formatted include {inc_sls} found in include " + f"in SLS '{saltenv}:{sls}'" ) log.error(msg) errors.append(msg) @@ -4117,11 +4116,9 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): p_comps.append("init") if level_count > len(p_comps): msg = ( - "Attempted relative include of '{}' " - "within SLS '{}:{}' " - "goes beyond top level package ".format( - inc_sls, saltenv, sls - ) + f"Attempted relative include of '{inc_sls}' " + f"within SLS '{saltenv}:{sls}' " + "goes beyond top level package " ) log.error(msg) errors.append(msg) @@ -4203,10 +4200,8 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None): ) elif len(resolved_envs) > 1: msg = ( - "Ambiguous include: Specified SLS {}: {} is available" - " on the salt master in multiple available saltenvs: {}".format( - env_key, inc_sls, ", ".join(resolved_envs) - ) + f"Ambiguous include: Specified SLS {env_key}: {inc_sls} is available" + f" on the salt master in multiple available saltenvs: {', '.join(resolved_envs)}" ) log.critical(msg) errors.append(msg) @@ -4257,33 +4252,33 @@ def _handle_state_decls(self, state, sls, saltenv, errors): """ Add sls and saltenv components to the state """ - for name in state: - if not isinstance(state[name], dict): - if name == "__extend__": + for id_, body in state.items(): + if not isinstance(body, dict): + if id_ == "__extend__": continue - if name == "__exclude__": + if id_ == "__exclude__": continue - if isinstance(state[name], str): + if isinstance(body, str): # Is this is a short state, it needs to be padded - if "." in state[name]: - comps = state[name].split(".") - state[name] = { + if "." in body: + state_name, function_name = body.split(".", maxsplit=1) + state[id_] = { "__sls__": sls, "__env__": saltenv, - comps[0]: [comps[1]], + state_name: [function_name], } continue - errors.append(f"ID {name} in SLS {sls} is not a dictionary") + errors.append(f"ID {id_} in SLS {sls} is not a dictionary") continue - skeys = set() - for key in list(state[name]): + state_keys = set() + for key, value in list(body.items()): if key.startswith("_"): continue - if not isinstance(state[name][key], list): + if not isinstance(value, (list, dict)): continue if "." in key: - comps = key.split(".") + state_name, function_name = key.split(".", maxsplit=1) # Salt doesn't support state files such as: # # /etc/redis/redis.conf: @@ -4294,21 +4289,24 @@ def _handle_state_decls(self, state, sls, saltenv, errors): # - mode: 644 # file.comment: # - regex: ^requirepass - if comps[0] in skeys: + if state_name in state_keys: errors.append( - "ID '{}' in SLS '{}' contains multiple state " - "declarations of the same type".format(name, sls) + f"ID '{id_}' in SLS '{sls}' contains multiple state " + "declarations of the same type" ) continue - state[name][comps[0]] = state[name].pop(key) - state[name][comps[0]].append(comps[1]) - skeys.add(comps[0]) + state[id_][state_name] = state[id_].pop(key) + if isinstance(value, dict): + state[id_][state_name]["fun"] = function_name + else: + state[id_][state_name].append(function_name) + state_keys.add(state_name) continue - skeys.add(key) - if "__sls__" not in state[name]: - state[name]["__sls__"] = sls - if "__env__" not in state[name]: - state[name]["__env__"] = saltenv + state_keys.add(key) + if "__sls__" not in body: + state[id_]["__sls__"] = sls + if "__env__" not in body: + state[id_]["__env__"] = saltenv def _handle_extend(self, state, sls, saltenv, errors): """ @@ -4323,9 +4321,7 @@ def _handle_extend(self, state, sls, saltenv, errors): for name in ext: if not isinstance(ext[name], dict): errors.append( - "Extension name '{}' in SLS '{}' is not a dictionary".format( - name, sls - ) + f"Extension name '{name}' in SLS '{sls}' is not a dictionary" ) continue if "__sls__" not in ext[name]: @@ -4351,9 +4347,7 @@ def _handle_exclude(self, state, sls, saltenv, errors): if "exclude" in state: exc = state.pop("exclude") if not isinstance(exc, list): - err = "Exclude Declaration in SLS {} is not formed as a list".format( - sls - ) + err = f"Exclude Declaration in SLS {sls} is not formed as a list" errors.append(err) state.setdefault("__exclude__", []).extend(exc) @@ -4375,7 +4369,7 @@ def render_highstate(self, matches, context=None): else: all_errors.append( "No matching salt environment for environment " - "'{}' found".format(saltenv) + f"'{saltenv}' found" ) # if we did not found any sls in the fileserver listing, this # may be because the sls was generated or added later, we can diff --git a/tests/pytests/functional/modules/state/test_state.py b/tests/pytests/functional/modules/state/test_state.py index 85a7fc4cc3e4..ba3b65347e8e 100644 --- a/tests/pytests/functional/modules/state/test_state.py +++ b/tests/pytests/functional/modules/state/test_state.py @@ -7,18 +7,13 @@ import pytest -import salt.loader import salt.modules.cmdmod as cmd import salt.modules.config as config import salt.modules.grains as grains import salt.modules.saltutil as saltutil import salt.modules.state as state_mod -import salt.utils.atomicfile -import salt.utils.files -import salt.utils.path import salt.utils.platform import salt.utils.state as state_util -import salt.utils.stringutils log = logging.getLogger(__name__) @@ -646,6 +641,36 @@ def test_pydsl(state, state_tree, tmp_path): assert testfile.exists() +def test_sls_with_state_args_dict(state, state_tree): + """ + Call sls file with state argument as a dict. + """ + sls_contents = """ + A: + test.succeed_without_changes: + name: echo foo + """ + with pytest.helpers.temp_file("testing.sls", sls_contents, state_tree): + ret = state.sls("testing") + for staterun in ret: + assert staterun.result is True + + +def test_sls_with_state_args_list(state, state_tree): + """ + Call sls file with state argument as a list. + """ + sls_contents = """ + A: + test.succeed_without_changes: + - name: echo foo + """ + with pytest.helpers.temp_file("testing.sls", sls_contents, state_tree): + ret = state.sls("testing") + for staterun in ret: + assert staterun.result is True + + def test_issues_7905_and_8174_sls_syntax_error(state, state_tree): """ Call sls file with yaml syntax error. @@ -680,12 +705,19 @@ def test_issues_7905_and_8174_sls_syntax_error(state, state_tree): "badlist1.sls", badlist_1_sls_contents, state_tree ), pytest.helpers.temp_file("badlist2.sls", badlist_2_sls_contents, state_tree): ret = state.sls("badlist1") - assert ret.failed - assert ret.errors == ["State 'A' in SLS 'badlist1' is not formed as a list"] + staterun = ret["cmd_|-A_|-A_|-run"] + assert staterun.result is False + assert ( + "A: command not found" in staterun.changes["stderr"] + or "'A' is not recognized as an internal or external command" + in staterun.changes["stderr"] + ) ret = state.sls("badlist2") assert ret.failed - assert ret.errors == ["State 'C' in SLS 'badlist2' is not formed as a list"] + assert ret.errors == [ + "State 'C' in SLS 'badlist2' is not formed as a list or dict" + ] def test_retry_option(state, state_tree): @@ -737,7 +769,7 @@ def test_retry_option_is_true(state, state_tree): for state_return in ret: assert state_return.result is False assert expected_comment in state_return.comment - assert state_return.full_return["duration"] >= 3 + assert state_return.full_return["duration"] >= 30 @pytest.mark.skip_initial_gh_actions_failure(skip=_check_skip) @@ -769,7 +801,7 @@ def test_retry_option_success(state, state_tree, tmp_path): assert state_return.result is True assert state_return.full_return["duration"] < duration # It should not take 2 attempts - assert "Attempt 2" not in state_return.comment + assert "Attempt 1" not in state_return.comment @pytest.mark.skip_on_windows( @@ -820,41 +852,44 @@ def test_retry_option_eventual_success(state, state_tree, tmp_path): testfile1 = tmp_path / "testfile-1" testfile2 = tmp_path / "testfile-2" + interval = 2 + def create_testfile(testfile1, testfile2): while True: if testfile1.exists(): break - time.sleep(2) + time.sleep(interval) testfile2.touch() thread = threading.Thread(target=create_testfile, args=(testfile1, testfile2)) - sls_contents = """ + sls_contents = f""" file_test_a: file.managed: - - name: {} + - name: {testfile1} - content: 'a' file_test: file.exists: - - name: {} + - name: {testfile2} - retry: until: True attempts: 5 - interval: 2 + interval: {interval} splay: 0 - require: - file_test_a - """.format( - testfile1, testfile2 - ) + """ with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree): thread.start() ret = state.sls("retry") - for state_return in ret: + for num, state_return in enumerate(ret): assert state_return.result is True - assert state_return.full_return["duration"] > 4 - # It should not take 5 attempts - assert "Attempt 5" not in state_return.comment + assert state_return.full_return["duration"] > interval + if num == 1: + # It should retry at least 1 time + assert "Attempt 1" in state_return.comment + # It should not take 5 attempts + assert "Attempt 5" not in state_return.comment @pytest.mark.skip_on_windows( @@ -867,45 +902,48 @@ def test_retry_option_eventual_success_parallel(state, state_tree, tmp_path): testfile1 = tmp_path / "testfile-1" testfile2 = tmp_path / "testfile-2" + interval = 2 + def create_testfile(testfile1, testfile2): while True: if testfile1.exists(): break - time.sleep(2) + time.sleep(interval) testfile2.touch() thread = threading.Thread(target=create_testfile, args=(testfile1, testfile2)) - sls_contents = """ + sls_contents = f""" file_test_a: file.managed: - - name: {} + - name: {testfile1} - content: 'a' file_test: file.exists: - - name: {} + - name: {testfile2} - retry: until: True attempts: 5 - interval: 2 + interval: {interval} splay: 0 - parallel: True - require: - file_test_a - """.format( - testfile1, testfile2 - ) + """ with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree): thread.start() ret = state.sls( "retry", __pub_jid="1" ) # Because these run in parallel we need a fake JID - for state_return in ret: + for num, state_return in enumerate(ret): log.debug("=== state_return %s ===", state_return) assert state_return.result is True - assert state_return.full_return["duration"] > 4 - # It should not take 5 attempts - assert "Attempt 5" not in state_return.comment + assert state_return.full_return["duration"] > interval + if num == 1: + # It should retry at least 1 time + assert "Attempt 1" in state_return.comment + # It should not take 5 attempts + assert "Attempt 5" not in state_return.comment def test_state_non_base_environment(state, state_tree_prod, tmp_path): @@ -1131,8 +1169,8 @@ def test_it(name): """ This should not fail on spawning platforms: requires_env.test_it: - - name: foo - - parallel: true + name: foo + parallel: true """ ) with pytest.helpers.temp_file( diff --git a/tests/pytests/integration/cli/test_salt_call.py b/tests/pytests/integration/cli/test_salt_call.py index f927f499c858..3948f620d108 100644 --- a/tests/pytests/integration/cli/test_salt_call.py +++ b/tests/pytests/integration/cli/test_salt_call.py @@ -81,6 +81,28 @@ def test_local_sls_call(salt_master, salt_call_cli): assert state_run_dict["changes"]["ret"] == "hello" +def test_local_sls_call_with_dict_args(salt_master, salt_call_cli): + sls_contents = """ + regular-module: + module.run: + name: test.echo + text: hello + """ + with salt_master.state_tree.base.temp_file("saltcalllocal.sls", sls_contents): + ret = salt_call_cli.run( + "--local", + "--file-root", + str(salt_master.state_tree.base.paths[0]), + "state.sls", + "saltcalllocal", + ) + assert ret.returncode == 0 + state_run_dict = next(iter(ret.data.values())) + assert state_run_dict["name"] == "test.echo" + assert state_run_dict["result"] is True + assert state_run_dict["changes"]["ret"] == "hello" + + def test_local_salt_call(salt_call_cli): """ This tests to make sure that salt-call does not execute the diff --git a/tests/pytests/integration/modules/test_state.py b/tests/pytests/integration/modules/test_state.py index 847a71df62c5..abd3cd853206 100644 --- a/tests/pytests/integration/modules/test_state.py +++ b/tests/pytests/integration/modules/test_state.py @@ -15,14 +15,12 @@ def test_logging_and_state_output_order(salt_master, salt_minion, salt_cli, tmp_ """ target_path = tmp_path / "file-target.txt" sls_name = "file-target" - sls_contents = """ + sls_contents = f""" add_contents_pillar_sls: file.managed: - - name: {} + - name: {target_path} - contents: foo - """.format( - target_path - ) + """ sls_tempfile = salt_master.state_tree.base.temp_file( f"{sls_name}.sls", sls_contents ) diff --git a/tests/pytests/integration/states/test_file.py b/tests/pytests/integration/states/test_file.py index d2a341910167..f298456e8865 100644 --- a/tests/pytests/integration/states/test_file.py +++ b/tests/pytests/integration/states/test_file.py @@ -1323,8 +1323,8 @@ def test_directory_recurse(salt_master, salt_call_cli, tmp_path, grains): with sls_tempfile: ret = salt_call_cli.run("state.sls", sls_name) key = f"file_|-{target_dir}_|-{target_dir}_|-directory" - assert key in ret.json - result = ret.json[key] + assert key in ret.data + result = ret.data[key] assert "changes" in result and result["changes"] # Permissions of file should not have changed. diff --git a/tests/pytests/pkg/integration/test_salt_call.py b/tests/pytests/pkg/integration/test_salt_call.py index c16ecb67481d..0be94e65b2e8 100644 --- a/tests/pytests/pkg/integration/test_salt_call.py +++ b/tests/pytests/pkg/integration/test_salt_call.py @@ -3,6 +3,9 @@ import pytest from pytestskipmarkers.utils import platform +import salt.version +from salt.utils.versions import Version + def test_salt_call_local(salt_call_cli): """ @@ -26,7 +29,7 @@ def test_salt_call(salt_call_cli, salt_master): @pytest.fixture def state_name(salt_master): - name = "some-test-state" + name = "state_name" sls_contents = """ test_foo: test.succeed_with_changes: @@ -46,13 +49,43 @@ def state_name(salt_master): yield name -def test_sls(salt_call_cli, salt_master, state_name): +@pytest.fixture +def state_name_dict_arg(salt_master): + name = "state_name_dict_arg" + sls_contents = """ + test_foo: + test.succeed_with_changes: + name: foo + """ + with salt_master.state_tree.base.temp_file(f"{name}.sls", sls_contents): + if not platform.is_windows() and not platform.is_darwin(): + subprocess.run( + [ + "chown", + "-R", + "salt:salt", + str(salt_master.state_tree.base.write_path), + ], + check=False, + ) + yield name + + +@pytest.mark.parametrize("fixture_name", ["state_name", "state_name_dict_arg"]) +def test_sls(salt_call_cli, salt_master, fixture_name, request): """ Test calling a sls file """ + min_version_required = Version("3008.0") + current_version = Version(salt.version.__version__) + if fixture_name == "state_name_dict_arg" and current_version < min_version_required: + pytest.skip( + f"requires Salt >= {min_version_required}, running {current_version}" + ) assert salt_master.is_running() + sls_id = request.getfixturevalue(fixture_name) - ret = salt_call_cli.run("state.apply", state_name) + ret = salt_call_cli.run("state.apply", sls_id) assert ret.returncode == 0 assert ret.data sls_ret = ret.data[next(iter(ret.data))] diff --git a/tests/pytests/unit/state/test_reactor_compiler.py b/tests/pytests/unit/state/test_reactor_compiler.py index 6e4a08019294..0e4451d51827 100644 --- a/tests/pytests/unit/state/test_reactor_compiler.py +++ b/tests/pytests/unit/state/test_reactor_compiler.py @@ -205,7 +205,7 @@ def test_compiler_pad_funcs_short_sls(minion_opts, tmp_path): } }, [ - "State 'master_pub' in SLS '/srv/reactor/start.sls' is not formed as a list" + "State 'master_pub' in SLS '/srv/reactor/start.sls' is not formed as a list or dict" ], ), ( diff --git a/tests/pytests/unit/state/test_state_basic.py b/tests/pytests/unit/state/test_state_basic.py index 00ef837f842e..2569f5497443 100644 --- a/tests/pytests/unit/state/test_state_basic.py +++ b/tests/pytests/unit/state/test_state_basic.py @@ -32,7 +32,7 @@ def test_state_args(): """ id_ = "/etc/bar.conf" state = "file" - high = OrderedDict( + high: salt.state.HighData = OrderedDict( [ ( "/etc/foo.conf", @@ -85,13 +85,60 @@ def test_state_args(): assert ret == {"order", "use"} +def test_state_args_as_dict() -> None: + """ + Testing state.state_args when this state is being used: + + /etc/foo.conf: + file.managed: + contents: "blah" + mkdirs: True + user: ch3ll + group: ch3ll + mode: 755 + + /etc/bar.conf: + file.managed: + use: + - file: /etc/foo.conf + """ + id_ = "/etc/bar.conf" + state = "file" + high: salt.state.HighData = { + "/etc/foo.conf": { + "file": { + "fun": "managed", + "contents": "blah", + "mkdirs": True, + "user": "ch3ll", + "group": "ch3ll", + "mode": 755, + "order": 10000, + }, + "__sls__": "test", + "__env__": "base", + }, + "/etc/bar.conf": { + "file": { + "fun": "managed", + "use": [{"file": "/etc/foo.conf"}], + "order": 10001, + }, + "__sls__": "test", + "__env__": "base", + }, + } + ret = salt.state.state_args(id_, state, high) + assert ret == {"order", "use"} + + def test_state_args_id_not_high(): """ Testing state.state_args when id_ is not in high """ id_ = "/etc/bar.conf2" state = "file" - high = OrderedDict( + high: salt.state.HighData = OrderedDict( [ ( "/etc/foo.conf", @@ -150,7 +197,7 @@ def test_state_args_state_not_high(): """ id_ = "/etc/bar.conf" state = "file2" - high = OrderedDict( + high: salt.state.HighData = OrderedDict( [ ( "/etc/foo.conf", diff --git a/tests/pytests/unit/state/test_state_compiler.py b/tests/pytests/unit/state/test_state_compiler.py index 122a12aaa769..32b5f731ccc1 100644 --- a/tests/pytests/unit/state/test_state_compiler.py +++ b/tests/pytests/unit/state/test_state_compiler.py @@ -720,10 +720,7 @@ def test_verify_retry_parsing(minion_opts): def test_render_requisite_require_disabled(minion_opts): - """ - Test that the state compiler correctly deliver a rendering - exception when a requisite cannot be resolved - """ + """Test disabling the require requisite via the options works""" with patch("salt.state.State._gather_pillar"): high_data = { "step_one": salt.state.HashableOrderedDict( @@ -817,6 +814,34 @@ def test_render_requisite_require_in_disabled(minion_opts): assert run_num == 0 +def test_render_with_dict_args(minion_opts): + """Test calling high state with state arguments specified as a dict""" + with patch("salt.state.State._gather_pillar"): + high_data: salt.state.HighData = { + "step_one": { + "test": { + "fun": "succeed_with_changes", + "require": [{"test": "step_two"}], + }, + "__sls__": "test.dict_args", + "__env__": "base", + }, + "step_two": { + "test": ["succeed_with_changes"], + "__env__": "base", + "__sls__": "test.dict_args", + }, + } + + state_obj = salt.state.State(minion_opts) + ret = state_obj.call_high(high_data) + assert isinstance(ret, dict) + run_num = ret["test_|-step_one_|-step_one_|-succeed_with_changes"][ + "__run_num__" + ] + assert run_num == 1 + + def test_call_chunk_sub_state_run(minion_opts): """ Test running a batch of states with an external runner From d175dbdb1a3da7248c0e67e5b2dd2118434f3e8a Mon Sep 17 00:00:00 2001 From: bdrx312 Date: Mon, 3 Nov 2025 21:21:58 -0500 Subject: [PATCH 2/3] Replace Union type hint in state.py with | --- salt/state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/state.py b/salt/state.py index bc4a222a1af8..370309df7c85 100644 --- a/salt/state.py +++ b/salt/state.py @@ -27,7 +27,7 @@ import time import traceback from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence -from typing import Any, TypeVar, Union +from typing import Any, TypeVar import networkx as nx @@ -69,13 +69,13 @@ # that have dict values. This could be cleaned up some by making # exclude and extend into dicts instead of lists so the type of all the values are # homogeneous. -HighData = dict[str, Union[Mapping[str, Any], list[Union[Mapping[str, Any], str]]]] +HighData = dict[str, Mapping[str, Any] | list[Mapping[str, Any] | str]] LowChunk = dict[str, Any] K = TypeVar("K") V = TypeVar("V") Pair = tuple[K, V] -HighDataStateArgsDef = Union[Mapping[K, V], Iterable[Union[Mapping[K, V], str]]] +HighDataStateArgsDef = Mapping[K, V] | Iterable[Mapping[K, V] | str] # These are keywords passed to state module functions which are to be used # by salt in this state module and not by the actual state module function From 6271504b8756f669de2d7f999399613b7b6e2088 Mon Sep 17 00:00:00 2001 From: bdrx312 Date: Thu, 13 Nov 2025 17:06:30 -0500 Subject: [PATCH 3/3] Update documentation --- doc/cheatsheet/salt.tex | 2 +- doc/glossary.rst | 5 + doc/ref/states/extend.rst | 96 ++++++- doc/ref/states/highstate.rst | 373 +++++++++++++++------------- doc/ref/states/ordering.rst | 2 +- doc/ref/states/requisites.rst | 317 +++++++++++------------ doc/topics/tutorials/states_pt1.rst | 4 +- doc/topics/tutorials/states_pt2.rst | 4 +- 8 files changed, 464 insertions(+), 339 deletions(-) diff --git a/doc/cheatsheet/salt.tex b/doc/cheatsheet/salt.tex index 9e85ebf14e26..e3e6709a0e0d 100644 --- a/doc/cheatsheet/salt.tex +++ b/doc/cheatsheet/salt.tex @@ -48,7 +48,7 @@ - running - enable: True /var/www/index.html: # ID declaration - file: # state declaration + file: # state module declaration - managed # function - source: salt://webserver/index.html - user: root diff --git a/doc/glossary.rst b/doc/glossary.rst index 612acef1c0e7..ab714e20d3ff 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -223,6 +223,11 @@ Glossary set of host machines. Often used to create and deploy private clouds. *See also*: :py:mod:`virt runner `. + SLS + Structured Layered State. The SLS is a representation of the state in + which a system should be in, and is set up to contain this data in a + simple format. This is often called configuration management. + SLS Module Contains a set of :term:`state declarations `. diff --git a/doc/ref/states/extend.rst b/doc/ref/states/extend.rst index 2526b1d98c9e..4c66ad11f9cd 100644 --- a/doc/ref/states/extend.rst +++ b/doc/ref/states/extend.rst @@ -1,3 +1,5 @@ +.. _extending-external-sls-data: + =========================== Extending External SLS Data =========================== @@ -9,10 +11,53 @@ overwritten or when a service needs to watch an additional state. The Extend Declaration ---------------------- -The standard way to extend is via the extend declaration. The extend -declaration is a top level declaration like ``include`` and encapsulates ID -declaration data included from other SLS files. A standard extend looks like -this: +A standard way to extend is via the extend declaration. The extend +declaration is a top level declaration like ``include`` that allows +overriding or augmenting state declarations from other SLS files. +Use ``extend`` to override arguments, append requisites, +or otherwise modify an existing ID without editing the original SLS. + +Overview +-------- + +- ``extend`` is a top-level mapping at the same syntactic level as ``include`` or a top-level ID. +- The SLS module that defines the target ID must be included so the ID exists before the + extend merge is applied. +- Requisite lists (for example ``watch`` and ``require``) are appended; most other keys + are replaced by the extend entry. +- Only one top-level ``extend`` mapping may appear in a single SLS file; later mappings + will overwrite earlier ones. + +Example +------- + +The following shows the original SLS entries (the files being extended) and an extending SLS +that includes them and declares a single ``extend`` block. + +Original: ``salt://http/init.sls`` + +.. code-block:: yaml + + apache: + pkg.installed: [] + file: + - name: /etc/httpd/conf/httpd.conf + - source: salt://http/httpd.conf + service.running: + - name: httpd + - watch: + - file: apache + +Original: ``salt://ssh/init.sls`` + +.. code-block:: yaml + + ssh-server: + pkg.installed: [] + service.running: + - name: sshd + +Extending SLS: ``salt://profile/webserver_extend.sls`` .. code-block:: yaml @@ -25,6 +70,7 @@ this: file: - name: /etc/httpd/conf/httpd.conf - source: salt://http/httpd2.conf + ssh-server: service: - watch: @@ -34,11 +80,40 @@ this: file.managed: - source: salt://ssh/banner -A few critical things happened here, first off the SLS files that are going to -be extended are included, then the extend dec is defined. Under the extend dec -2 IDs are extended, the apache ID's file state is overwritten with a new name -and source. Then the ssh server is extended to watch the banner file in -addition to anything it is already watching. +Behavior for this example +------------------------- + +- The ``apache:file`` mapping in the extending SLS overrides with the + ``name`` and ``source`` values from the original ``file`` mapping + in ``http/init.sls`` with the values supplied under ``extend``. +- The ``ssh-server:service:watch`` list is appended with ``file: /etc/ssh/banner``; any + existing watch entries declared in ``ssh/init.sls`` are preserved. +- The banner resource is declared locally (``/etc/ssh/banner``) so the appended watch has + a concrete state to observe; if the resource were absent from the compiled data the + relationship would be invalid. + +Minimal patterns +---------------- + +Replace a mapping (overwrite): + +.. code-block:: yaml + + extend: + apache: + file: + - name: /etc/httpd/conf/httpd.conf + - source: salt://http/httpd2.conf + +Append to a requisite list (merge): + +.. code-block:: yaml + + extend: + ssh-server: + service: + - watch: + - file: /etc/ssh/banner Extend is a Top Level Declaration --------------------------------- @@ -85,6 +160,9 @@ cleanly defined like so: - watch_in: - service: ssh-server +:ref:`State Requisites ` + + Rules to Extend By ------------------ There are a few rules to remember when extending states: diff --git a/doc/ref/states/highstate.rst b/doc/ref/states/highstate.rst index f5326df63422..17bb2c20c154 100644 --- a/doc/ref/states/highstate.rst +++ b/doc/ref/states/highstate.rst @@ -32,7 +32,7 @@ Configurable via :conf_master:`state_top`. Include declaration ------------------- -Defines a list of :ref:`module-reference` strings to include in this ``SLS``. +Defines a list of :ref:`sls-module-reference` strings to include in this ``SLS``. Occurs only in the top level of the SLS data structure. @@ -44,13 +44,14 @@ Example: - edit.vim - http.server -.. _module-reference: +.. _sls-module-reference: -Module reference ----------------- +SLS module reference +-------------------- -The name of a SLS module defined by a separate SLS file and residing on -the Salt Master. A module named ``edit.vim`` is a reference to the SLS +A reference to an SLS module defined by a separate SLS file or +directory residing on the Salt Master. +For example ``edit.vim`` is a reference to the SLS file ``salt://edit/vim.sls``. .. _id-declaration: @@ -58,14 +59,16 @@ file ``salt://edit/vim.sls``. ID declaration -------------- -Defines an individual :ref:`highstate ` component. Always -references a value of a dictionary containing keys referencing -:ref:`state-declaration` and :ref:`requisite-declaration`. Can be overridden by -a :ref:`name-declaration` or a :ref:`names-declaration`. +A label that identifies an individual :ref:`highstate ` component. +The ID is a reference to a dictionary containing entries of one or more +:ref:`state-declaration` components. +The ID is used as an implicit name argument for the state function for any of +the referenced state declarations that do not provide an +explicit name with a :ref:`name-declaration` or a :ref:`names-declaration`. Occurs on the top level or under the :ref:`extend-declaration`. -Must be unique across entire state tree. If the same ID declaration is +Must be unique across the entire state tree. If the same ID declaration is used twice, then a compilation error will occur. .. note:: Naming gotchas @@ -73,88 +76,35 @@ used twice, then a compilation error will occur. In Salt versions earlier than 0.9.7, ID declarations containing dots would result in unpredictable output. -.. _extend-declaration: - -Extend declaration ------------------- - -Extends a :ref:`name-declaration` from an included ``SLS module``. The -keys of the extend declaration always refer to an existing -:ref:`id-declaration` which have been defined in included ``SLS modules``. - -Occurs only in the top level and defines a dictionary. - -States cannot be extended more than once in a single state run. - -Extend declarations are useful for adding-to or overriding parts of a -:ref:`state-declaration` that is defined in another ``SLS`` file. In the -following contrived example, the shown ``mywebsite.sls`` file is ``include`` --ing and ``extend`` -ing the ``apache.sls`` module in order to add a ``watch`` -declaration that will restart Apache whenever the Apache configuration file, -``mywebsite`` changes. - -.. code-block:: yaml - - include: - - apache - - extend: - apache: - service: - - watch: - - file: mywebsite - - mywebsite: - file.managed: - - name: /var/www/mysite - -.. seealso:: watch_in and require_in - - Sometimes it is more convenient to use the :ref:`watch_in - ` or :ref:`require_in ` syntax - instead of extending another ``SLS`` file. - - :ref:`State Requisites ` - .. _state-declaration: State declaration ----------------- -A list which contains one string defining the :ref:`function-declaration` and -any number of :ref:`function-arg-declaration` dictionaries. +A state declaration consists of a :ref:`state-module-declaration`, +a :ref:`function-declaration` and any number of +:ref:`function-arg-declaration` items. Can, optionally, contain a number of additional components like the -name override components — :ref:`name ` and +name components — :ref:`name ` and :ref:`names `. Can also contain :ref:`requisite declarations `. Occurs under an :ref:`ID-declaration`. -.. _requisite-declaration: +.. _state-module-declaration: -Requisite declaration ---------------------- - -A list containing :ref:`requisite references `. - -Used to build the action dependency tree. While Salt states are made to -execute in a deterministic order, this order is managed by requiring -and watching other Salt states. - -Occurs as a list component under a :ref:`state-declaration` or as a -key under an :ref:`ID-declaration`. - -.. _requisite-reference: +State Module declaration +------------------------ -Requisite reference -------------------- +Names the Salt state module (for example ``file``, ``pkg``, +``service``) that provides the function invoked for the state. -A single key dictionary. The key is the name of the referenced -:ref:`state-declaration` and the value is the ID of the referenced -:ref:`ID-declaration`. +Occurs in the key/identifier of the :ref:`state-declaration` dictionary +under an :ref:`ID declaration `. -Occurs as a single index in a :ref:`requisite-declaration` list. +Multiple state module declarations can be specified under the same +ID declaration but per ID each state module must be unique. .. _function-declaration: @@ -164,6 +114,8 @@ Function declaration The name of the function to call within the state. A state declaration can contain only a single function declaration. +Occurs in the :ref:`state-declaration` + For example, the following state declaration calls the :mod:`installed ` function in the ``pkg`` state module: @@ -172,33 +124,30 @@ For example, the following state declaration calls the :mod:`installed httpd: pkg.installed: [] -The function can be declared inline with the state as a shortcut. -The actual data structure is compiled to this form: +The function can be declared combined inline with the +:ref:`state-module-declaration` separated by a period `.` +as a short form dot notation. +The actual data structure is compiled to the long form shown below: .. code-block:: yaml httpd: pkg: - - installed - -Where the function is a string in the body of the state declaration. -Technically when the function is declared in dot notation the compiler -converts it to be a string in the state declaration list. Note that the -use of the first example more than once in an ID declaration is invalid -yaml. + - fun: installed -INVALID: +If no arguments need to be given to the function, the argument list can be +omitted and the state declaration can be given as a single string in short form: .. code-block:: yaml httpd: pkg.installed - service.running -When passing a function without arguments and another state declaration -within a single ID declaration, then the long or "standard" format -needs to be used since otherwise it does not represent a valid data -structure. +Note that this string short form cannot be more than once per ID declaration. +When passing a function without arguments and another state declaration within +a single ID declaration component, then an empty list or dictionary needs +to be specified as the arguments value since otherwise it does not represent +a valid data structure. VALID: @@ -206,24 +155,43 @@ VALID: httpd: pkg.installed: [] - service.running: [] + service.running: {} -Occurs as the only index in the :ref:`state-declaration` list. +INVALID: + +.. code-block:: yaml + + httpd: + pkg.installed + service.running .. _function-arg-declaration: Function arg declaration ------------------------ -A single key dictionary referencing a Python type which is to be passed -to the named :ref:`function-declaration` as a parameter. The type must -be the data type expected by the function. +A argument consisting of keyword and value which is to be passed to the named +:ref:`function-declaration` as a parameter. The type of each value must be +the data type expected by the function. +The function arguments can be specified as a dictionary or as a list with each +item as single item dictionary. Occurs under a :ref:`function-declaration`. For example in the following state declaration ``user``, ``group``, and ``mode`` are passed as arguments to the :mod:`managed -` function in the ``file`` state module: +` function in the ``file`` state module by +specifying the arguments as a dictionary: + +.. code-block:: yaml + + /etc/http/conf/http.conf: + file.managed: + user: root + group: root + mode: '0644' + +In this example the arguments are specified as a list of single item dictionaries: .. code-block:: yaml @@ -231,20 +199,82 @@ For example in the following state declaration ``user``, ``group``, and file.managed: - user: root - group: root - - mode: 644 + - mode: '0644' + +.. _requisite-declaration: + +Requisite declaration +--------------------- + +A key value pair of key that is a :ref:`requisite type ` +with a value that is a list containing :ref:`requisite references `. + +Used to build the action dependency tree. While Salt states are made to +execute in a deterministic order, this order is managed by requiring +and watching other Salt states. + +Occurs as a component in a :ref:`state-declaration`. + +.. code-block:: yaml + + : + - + - + +.. code-block:: yaml + + require: # requisite type + - file: /etc/http/conf/http.conf + - service: httpd + - httpd + +See requisites: :ref:`Requisites ` + +.. _requisite-type-declaration: + +Requisite type declaration +-------------------------- + +The type of the dependency/requisite relationship. + +Occurs in a :ref:`requisite-declaration`. + +See :ref:`requisite-types` + +.. _requisite-reference: + +Requisite reference +------------------- + +One of the items in a :ref:`requisite-declaration` list that specifies +a target of the requisite. + +Either + +- A key value pair where the key is the name of the referenced + :ref:`state-module-declaration` and the value is the ID of the referenced + :ref:`ID-declaration` or the :ref:`name ` of the + referenced :ref:`state-declaration`. + For example the reference `file: vim` is a reference a state declaration + with the to the state module ``file`` with the ID or name ``vim`` +- A single string identifier. In version 2016.3.0, the state module name was + made optional. If the state module is omitted, all states matching the + identifier will be required, regardless of which state module they are using. + +Occurs in a :ref:`requisite-declaration` list. .. _name-declaration: Name declaration ---------------- -Overrides the ``name`` argument of a :ref:`state-declaration`. If +Specifies the ``name`` argument of a :ref:`state-declaration`. If ``name`` is not specified the :ref:`ID-declaration` satisfies the ``name`` argument. -The name is always a single key dictionary referencing a string. +The name is a string. -Overriding ``name`` is useful for a variety of scenarios. +Including a ``name`` declaration is useful for a variety of scenarios. For example, avoiding clashing ID declarations. The following two state declarations cannot both have ``/etc/motd`` as the ID declaration: @@ -253,13 +283,13 @@ declarations cannot both have ``/etc/motd`` as the ID declaration: motd_perms: file.managed: - - name: /etc/motd - - mode: 644 + name: /etc/motd + mode: '0644' motd_quote: file.append: - - name: /etc/motd - - text: "Of all smells, bread; of all tastes, salt." + name: /etc/motd + text: "Of all smells, bread; of all tastes, salt." Another common reason to override ``name`` is if the ID declaration is long and needs to be referenced in multiple places. In the example below it is much @@ -270,18 +300,18 @@ easier to specify ``mywebsite`` than to specify mywebsite: file.managed: - - name: /etc/apache2/sites-available/mywebsite.com - - source: salt://mywebsite.com + name: /etc/apache2/sites-available/mywebsite.com + source: salt://mywebsite.com a2ensite mywebsite.com: cmd.wait: - - unless: test -L /etc/apache2/sites-enabled/mywebsite.com - - watch: + unless: test -L /etc/apache2/sites-enabled/mywebsite.com + watch: - file: mywebsite apache2: service.running: - - watch: + watch: - file: mywebsite .. _names-declaration: @@ -298,7 +328,7 @@ For example, given the following state declaration: python-pkgs: pkg.installed: - - names: + names: - python-django - python-crypto - python-yaml @@ -324,17 +354,30 @@ dictionary level. .. code-block:: yaml - ius: - pkgrepo.managed: - - humanname: IUS Community Packages for Enterprise Linux 6 - $basearch - - gpgcheck: 1 - - baseurl: http://mirror.rackspace.com/ius/stable/CentOS/6/$basearch - - gpgkey: http://dl.iuscommunity.org/pub/ius/IUS-COMMUNITY-GPG-KEY - - names: - - ius - - ius-devel: + ius: + pkgrepo.managed: + humanname: IUS Community Packages for Enterprise Linux 6 - $basearch + gpgcheck: 1 + baseurl: http://mirror.rackspace.com/ius/stable/CentOS/6/$basearch + gpgkey: http://dl.iuscommunity.org/pub/ius/IUS-COMMUNITY-GPG-KEY + names: + - ius + - ius-devel: - baseurl: http://mirror.rackspace.com/ius/development/CentOS/6/$basearch +.. _extend-declaration: + +Extend declaration +------------------ + +Extends a :ref:`state-declaration` from an included ``SLS module``. + +Occurs only in the top level and defines a dictionary. + +States cannot be extended more than once in a single state run. + +See extending states: :ref:`Extending External SLS Data ` + .. _states-highstate-example: Large example @@ -346,58 +389,56 @@ components. .. code-block:: yaml : - - - - + - + - : : [] + # inline short form dot notation for function declaration with dictionary + # for function arguments, names, and requisites + : + .: + + + + + + + + # inline short form dot notation for function declaration with list + # for function arguments, names, and requisites + : + .: + - + - + - + - + - + - - # standard declaration - - : - : - - - - - - - - - - : - - : - - - - - - - # inline function and names - + # multiple states for single id : - .: - - - - - - - - : - - - - - - - - : - - - - - + .: + s... + + s... + .: + s... + + s... - # multiple states for single id + # traditional declaration : - : - - - - - - : - - : - - - : - - - - - - : - - - - - - : - - + : + - + - s... + - + - s... + : + - + - s... + - + - s... diff --git a/doc/ref/states/ordering.rst b/doc/ref/states/ordering.rst index 856da2eb0b61..4f07d2fbd0b3 100644 --- a/doc/ref/states/ordering.rst +++ b/doc/ref/states/ordering.rst @@ -78,7 +78,7 @@ can be evaluated to see if they have executed correctly. Require statements can refer to any state defined in Salt. The basic examples are `pkg`, `service`, and `file`, but any used state can be referenced. -In addition to state declarations such as pkg, file, etc., **sls** type requisites +In addition to state module declarations such as pkg, file, etc., **sls** type requisites are also recognized, and essentially allow 'chaining' of states. This provides a mechanism to ensure the proper sequence for complex state formulas, especially when the discrete states are split or groups into separate sls files: diff --git a/doc/ref/states/requisites.rst b/doc/ref/states/requisites.rst index c879e85f910e..0011fd4b46ce 100644 --- a/doc/ref/states/requisites.rst +++ b/doc/ref/states/requisites.rst @@ -9,8 +9,8 @@ Requisites The Salt requisite system is used to create relationships between states. This provides a method to easily define inter-dependencies between states. These -dependencies are expressed by declaring the relationships using state names -and IDs or names. The generalized form of a requisite target is ``: +dependencies are expressed by declaring the relationships using state module names +and IDs or names. The generalized form of a requisite target is ``: ``. The specific form is defined as a :ref:`Requisite Reference `. @@ -23,10 +23,10 @@ package could not be installed, Salt will not try to manage the service. nginx: pkg.installed: - - name: nginx-light + name: nginx-light service.running: - - enable: True - - require: + enable: True + require: - pkg: nginx Without the requisite defined, salt would attempt to install the package and @@ -75,13 +75,13 @@ so either of the following versions for "Extract server package" is correct: # Match by ID declaration Extract server package: archive.extracted: - - onchanges: + onchanges: - file: Deploy server package # Match by name parameter Extract server package: archive.extracted: - - onchanges: + onchanges: - file: /usr/local/share/myapp.tar.xz Wildcard matching in requisites @@ -105,7 +105,7 @@ will reload/restart the service: apache2: service.running: - - watch: + watch: - file: /etc/apache2/* A leading or bare ``*`` must be quoted to avoid confusion with YAML references: @@ -114,7 +114,7 @@ A leading or bare ``*`` must be quoted to avoid confusion with YAML references: /etc/letsencrypt/renewal-hooks/deploy/install.sh: cmd.run: - - onchanges: + onchanges: - acme: '*' @@ -129,9 +129,10 @@ module they are using. .. code-block:: yaml - - require: + require: - vim +.. _requisite-types: Requisites Types ---------------- @@ -178,7 +179,7 @@ In the following example, the ``service`` state will not be checked unless both nginx: service.running: - - require: + require: - file: /etc/nginx/nginx.conf - file: /etc/nginx/conf.d/ssl.conf @@ -196,7 +197,7 @@ file: bar: pkg.installed: - - require: + require: - sls: foo This will add a ``require`` to all of the state declarations found in the given @@ -222,11 +223,11 @@ if any of the watched states changes. myservice: file.managed: - - name: /etc/myservice/myservice.conf - - source: salt://myservice/files/myservice.conf + name: /etc/myservice/myservice.conf + source: salt://myservice/files/myservice.conf cmd.run: - - name: /usr/local/sbin/run-build - - onchanges: + name: /usr/local/sbin/run-build + onchanges: - file: /etc/myservice/myservice.conf In the example above, ``cmd.run`` will run only if there are changes in the @@ -239,11 +240,11 @@ correct choice, as seen in this next example. myservice: file.managed: - - name: /etc/myservice/myservice.conf - - source: salt://myservice/files/myservice.conf + name: /etc/myservice/myservice.conf + source: salt://myservice/files/myservice.conf cmd.run: - - name: /usr/local/sbin/run-build - - onchanges_in: # <-- broken logic + name: /usr/local/sbin/run-build + onchanges_in: # <-- broken logic - file: /etc/myservice/myservice.conf @@ -295,11 +296,11 @@ to Salt ensuring that the service is running. ntpd: service.running: - - watch: + watch: - file: /etc/ntp.conf file.managed: - - name: /etc/ntp.conf - - source: salt://ntp/files/ntp.conf + name: /etc/ntp.conf + source: salt://ntp/files/ntp.conf Another useful example of ``watch`` is using salt to ensure a configuration file is present and in a correct state, ensure the service is running, and trigger @@ -310,12 +311,12 @@ dropping any connections. nginx: service.running: - - reload: True - - watch: + reload: True + watch: - file: nginx file.managed: - - name: /etc/nginx/conf.d/tls-settings.conf - - source: salt://nginx/files/tls-settings.conf + name: /etc/nginx/conf.d/tls-settings.conf + source: salt://nginx/files/tls-settings.conf .. note:: @@ -391,14 +392,14 @@ every necessary change. You might be tempted to write something like this: httpd: service.running: - - enable: True - - watch: + enable: True + watch: - file: httpd-config httpd-config: file.managed: - - name: /etc/httpd/conf/httpd.conf - - source: salt://httpd/files/apache.conf + name: /etc/httpd/conf/httpd.conf + source: salt://httpd/files/apache.conf If your service is already running but not enabled, you might expect that Salt will be able to tell that since the config file changed your service needs to @@ -413,18 +414,18 @@ simply make sure that ``service.running`` is in a state on its own: enable-httpd: service.enabled: - - name: httpd + name: httpd start-httpd: service.running: - - name: httpd - - watch: + name: httpd + watch: - file: httpd-config httpd-config: file.managed: - - name: /etc/httpd/conf/httpd.conf - - source: salt://httpd/files/apache.conf + name: /etc/httpd/conf/httpd.conf + source: salt://httpd/files/apache.conf Now that ``service.running`` is its own state, changes to ``service.enabled`` will no longer prevent ``mod_watch`` from getting triggered, so your ``httpd`` @@ -445,30 +446,30 @@ created by ``listen`` will execute at the end of the state run. restart-apache2: service.running: - - name: apache2 - - listen: + name: apache2 + listen: - file: /etc/apache2/apache2.conf configure-apache2: file.managed: - - name: /etc/apache2/apache2.conf - - source: salt://apache2/apache2.conf + name: /etc/apache2/apache2.conf + source: salt://apache2/apache2.conf This example will cause apache2 to restart when the apache2.conf file is changed, but the apache2 restart will happen at the end of the state run. .. code-block:: yaml - restart-apache2: - service.running: - - name: apache2 + restart-apache2: + service.running: + name: apache2 - configure-apache2: - file.managed: - - name: /etc/apache2/apache2.conf - - source: salt://apache2/apache2.conf - - listen_in: - - service: apache2 + configure-apache2: + file.managed: + name: /etc/apache2/apache2.conf + source: salt://apache2/apache2.conf + listen_in: + - service: apache2 This example does the same as the above example, but puts the state argument on the file resource, rather than the service resource. @@ -494,14 +495,14 @@ is the pre-required state. graceful-down: cmd.run: - - name: service apache graceful - - prereq: + name: service apache graceful + prereq: - file: site-code site-code: file.recurse: - - name: /opt/site_code - - source: salt://site/code + name: /opt/site_code + source: salt://site/code In this case, the apache server will only be shut down if the site-code state expects to deploy fresh code via the file.recurse call. The site-code deployment @@ -542,28 +543,28 @@ The ``onfail`` requisite is applied in the same way as ``require`` and ``watch`` primary_mount: mount.mounted: - - name: /mnt/share - - device: 10.0.0.45:/share - - fstype: nfs + name: /mnt/share + device: 10.0.0.45:/share + fstype: nfs backup_mount: mount.mounted: - - name: /mnt/share - - device: 192.168.40.34:/share - - fstype: nfs - - onfail: + name: /mnt/share + device: 192.168.40.34:/share + fstype: nfs + onfail: - mount: primary_mount .. code-block:: yaml build_site: cmd.run: - - name: /srv/web/app/build_site + name: /srv/web/app/build_site notify-build_failure: hipchat.send_message: - - room_id: 123456 - - message: "Building website fail on {{ grains['id'] }}" + room_id: 123456 + message: "Building website fail on {{ grains['id'] }}" The default behavior of the ``onfail`` when multiple requisites are listed is @@ -577,17 +578,17 @@ form: test_site_a: cmd.run: - - name: ping -c1 10.0.0.1 + name: ping -c1 10.0.0.1 test_site_b: cmd.run: - - name: ping -c1 10.0.0.2 + 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: + room_id: 123456 + message: "Both primary and backup sites are down!" + onfail_all: - cmd: test_site_a - cmd: test_site_b @@ -625,17 +626,17 @@ id declaration. This is useful when many files need to have the same defaults. /etc/foo.conf: file.managed: - - source: salt://foo.conf - - template: jinja - - mkdirs: True - - user: apache - - group: apache - - mode: 755 + source: salt://foo.conf + template: jinja + mkdirs: True + user: apache + group: apache + mode: 755 /etc/bar.conf: file.managed: - - source: salt://bar.conf - - use: + source: salt://bar.conf + use: - file: /etc/foo.conf The ``use`` statement was developed primarily for the networking states but @@ -669,14 +670,14 @@ the exact same dependency mapping. httpd: pkg.installed: [] service.running: - - require: + require: - pkg: httpd .. code-block:: yaml httpd: pkg.installed: - - require_in: + require_in: - service: httpd service.running: [] @@ -689,9 +690,9 @@ nginx`` requisite. nginx: pkg.installed: [] service.running: - - enable: True - - reload: True - - require: + enable: True + reload: True + require: - pkg: nginx php.sls @@ -703,7 +704,7 @@ php.sls php: pkg.installed: - - require_in: + require_in: - service: httpd mod_python.sls @@ -715,7 +716,7 @@ mod_python.sls mod_python: pkg.installed: - - require_in: + require_in: - service: httpd Now the httpd server will only start if both php and mod_python are first verified to @@ -754,18 +755,18 @@ from ``all()`` to ``any()``. A: cmd.run: - - name: echo A - - require_any: + name: echo A + require_any: - cmd: B - cmd: C B: cmd.run: - - name: echo B + name: echo B C: cmd.run: - - name: /bin/false + name: /bin/false In this example ``A`` will run because at least one of the requirements specified, ``B`` or ``C``, will succeed. @@ -777,18 +778,18 @@ In this example ``A`` will run because at least one of the requirements specifie /etc/myservice/myservice.conf: file.managed: - - source: salt://myservice/files/myservice.conf + source: salt://myservice/files/myservice.conf /etc/yourservice/yourservice.conf: file.managed: - - source: salt://yourservice/files/yourservice.conf + source: salt://yourservice/files/yourservice.conf /usr/local/sbin/myservice/post-changes-hook.sh cmd.run: - - onchanges_any: + onchanges_any: - file: /etc/myservice/myservice.conf - file: /etc/your_service/yourservice.conf - - require: + require: - pkg: myservice In this example, `cmd.run` would be run only if either of the `file.managed` @@ -817,12 +818,12 @@ See :ref:`Reloading Modules `. grains_refresh: module.run: - - name: saltutil.refresh_grains - - reload_grains: true + name: saltutil.refresh_grains + reload_grains: true grains_read: module.run: - - name: grains.items + name: grains.items .. _unless-requisite: @@ -847,7 +848,7 @@ concept of ``True`` and ``False``. vim: pkg.installed: - - unless: + unless: - rpm -q vim-enhanced - ls /usr/bin/vim @@ -866,10 +867,10 @@ For example: deploy_app: cmd.run: - - names: + names: - first_deploy_cmd - second_deploy_cmd - - unless: some_check + unless: some_check In the above case, ``some_check`` will be run prior to _each_ name -- once for ``first_deploy_cmd`` and a second time for ``second_deploy_cmd``. @@ -884,9 +885,9 @@ In the above case, ``some_check`` will be run prior to _each_ name -- once for install apache on debian based distros: cmd.run: - - name: make install - - cwd: /path/to/dir/whatever-2.1.5/ - - unless: + name: make install + cwd: /path/to/dir/whatever-2.1.5/ + unless: - fun: file.file_exists path: /usr/local/bin/whatever @@ -894,10 +895,10 @@ In the above case, ``some_check`` will be run prior to _each_ name -- once for set mysql root password: debconf.set: - - name: mysql-server-5.7 - - data: + name: mysql-server-5.7 + data: 'mysql-server/root_password': {'type': 'password', 'value': {{pillar['mysql.pass']}} } - - unless: + unless: - fun: pkg.version args: - mysql-server-5.7 @@ -910,8 +911,8 @@ In the above case, ``some_check`` will be run prior to _each_ name -- once for test: test.nop: - - name: foo - - unless: + name: foo + unless: - fun: consul.get consul_url: http://127.0.0.1:8500 key: not-existing @@ -933,11 +934,11 @@ In the above case, ``some_check`` will be run prior to _each_ name -- once for jim_nologin: user.present: - - name: jim - - shell: /sbin/nologin - - unless: + name: jim + shell: /sbin/nologin + unless: - echo hello world - - cmd_opts_exclude: + cmd_opts_exclude: - shell .. _onlyif-requisite: @@ -962,19 +963,19 @@ concept of ``True`` and ``False``. stop-volume: module.run: - - name: glusterfs.stop_volume - - m_name: work - - onlyif: + name: glusterfs.stop_volume + m_name: work + onlyif: - gluster volume status work - - order: 1 + order: 1 remove-volume: module.run: - - name: glusterfs.delete - - m_name: work - - onlyif: + name: glusterfs.delete + m_name: work + onlyif: - gluster volume info work - - watch: + watch: - cmd: stop-volume The above example ensures that the stop_volume and delete modules only run @@ -990,15 +991,15 @@ if the gluster commands return a 0 ret value. install apache on redhat based distros: pkg.latest: - - name: httpd - - onlyif: + name: httpd + onlyif: - fun: match.grain tgt: 'os_family:RedHat' install apache on debian based distros: pkg.latest: - - name: apache2 - - onlyif: + name: apache2 + onlyif: - fun: match.grain tgt: 'os_family:Debian' @@ -1006,8 +1007,8 @@ if the gluster commands return a 0 ret value. arbitrary file example: file.touch: - - name: /path/to/file - - onlyif: + name: /path/to/file + onlyif: - fun: file.search args: - /etc/crontab @@ -1021,8 +1022,8 @@ if the gluster commands return a 0 ret value. test: test.nop: - - name: foo - - onlyif: + name: foo + onlyif: - fun: consul.get consul_url: http://127.0.0.1:8500 key: does-exist @@ -1044,11 +1045,11 @@ if the gluster commands return a 0 ret value. jim_nologin: user.present: - - name: jim - - shell: /sbin/nologin - - onlyif: + name: jim + shell: /sbin/nologin + onlyif: - echo hello world - - cmd_opts_exclude: + cmd_opts_exclude: - shell .. _creates-requisite: @@ -1068,8 +1069,8 @@ should execute. This was previously used by the :mod:`cmd ` and contrived creates example: file.touch: - - name: /path/to/file - - creates: /path/to/file + name: /path/to/file + creates: /path/to/file ``creates`` also accepts a list of files, in which case this state will run if **any** of the files do not exist: @@ -1078,8 +1079,8 @@ run if **any** of the files do not exist: creates list: file.cmd: - - name: /path/to/command - - creates: + name: /path/to/command + creates: - /path/file - /path/file2 @@ -1096,9 +1097,9 @@ the command in the ``cmd.run`` module. django: pip.installed: - - name: django >= 1.6, <= 1.7 - - runas: daniel - - require: + name: django >= 1.6, <= 1.7 + runas: daniel + require: - pkg: python-pip In the above state, the pip command run by ``cmd.run`` will be run by the daniel user. @@ -1116,9 +1117,9 @@ is specified. It will be set when ``runas_password`` is defined in the state. run_script: cmd.run: - - name: Powershell -NonInteractive -ExecutionPolicy Bypass -File C:\\Temp\\script.ps1 - - runas: frank - - runas_password: supersecret + name: Powershell -NonInteractive -ExecutionPolicy Bypass -File C:\\Temp\\script.ps1 + runas: frank + runas_password: supersecret In the above state, the Powershell script run by ``cmd.run`` will be run by the frank user with the password ``supersecret``. @@ -1140,10 +1141,10 @@ the salt-minion. comment-repo: file.replace: - - name: /etc/yum.repos.d/fedora.repo - - pattern: '^enabled=0' - - repl: enabled=1 - - check_cmd: + name: /etc/yum.repos.d/fedora.repo + pattern: '^enabled=0' + repl: enabled=1 + check_cmd: - "! grep 'enabled=0' /etc/yum.repos.d/fedora.repo" This will attempt to do a replace on all ``enabled=0`` in the .repo file, and @@ -1188,8 +1189,8 @@ of the state gets added to the tag. nano_stuff: pkg.installed: - - name: nano - - fire_event: True + name: nano + fire_event: True In the following example instead of setting `fire_event` to `True`, `fire_event` is set to an arbitrary string, which will cause the event to be @@ -1200,8 +1201,8 @@ sent with this tag: nano_stuff: pkg.installed: - - name: nano - - fire_event: custom/tag/nano/finished + name: nano + fire_event: custom/tag/nano/finished Retrying States =============== @@ -1237,8 +1238,8 @@ up to an additional ``10`` seconds: my_retried_state: pkg.installed: - - name: nano - - retry: + name: nano + retry: attempts: 5 until: True interval: 60 @@ -1252,8 +1253,8 @@ returns ``True``. install_nano: pkg.installed: - - name: nano - - retry: True + name: nano + retry: True The following example will run the file.exists state every ``30`` seconds up to ``15`` times or until the file exists (i.e. the state returns ``True``). @@ -1262,8 +1263,8 @@ or until the file exists (i.e. the state returns ``True``). wait_for_file: file.exists: - - name: /path/to/file - - retry: + name: /path/to/file + retry: attempts: 15 interval: 30 @@ -1288,8 +1289,8 @@ For example: wait_for_file: file.exists: - - name: /path/to/file - - retry: + name: /path/to/file + retry: attempts: 10 interval: 2 splay: 5 @@ -1330,7 +1331,7 @@ states, but it is now a global state argument that can be applied to any state. cleanup_script: cmd.script: - - name: salt://myapp/files/my_script.sh - - umask: "077" - - onchanges: + name: salt://myapp/files/my_script.sh + umask: "077" + onchanges: - file: /some/file diff --git a/doc/topics/tutorials/states_pt1.rst b/doc/topics/tutorials/states_pt1.rst index 5e164c39fcd6..4c9da81ecd80 100644 --- a/doc/topics/tutorials/states_pt1.rst +++ b/doc/topics/tutorials/states_pt1.rst @@ -83,7 +83,7 @@ named ``webserver.sls``, containing the following: .. code-block:: yaml apache: # ID declaration - pkg: # state declaration + pkg: # state module declaration - installed # function declaration The first line, called the :ref:`id-declaration`, is an arbitrary identifier. @@ -95,7 +95,7 @@ In this case it defines the name of the package to be installed. OS or distro — for example, on Fedora it is ``httpd`` but on Debian/Ubuntu it is ``apache2``. -The second line, called the :ref:`state-declaration`, defines which of the Salt +The second line, called the :ref:`state-module-declaration`, defines which of the Salt States we are using. In this example, we are using the :mod:`pkg state ` to ensure that a given package is installed. diff --git a/doc/topics/tutorials/states_pt2.rst b/doc/topics/tutorials/states_pt2.rst index 06148b858b9a..ea399e059b89 100644 --- a/doc/topics/tutorials/states_pt2.rst +++ b/doc/topics/tutorials/states_pt2.rst @@ -61,7 +61,7 @@ installed and running. Include the following at the bottom of your - pkg: apache /var/www/index.html: # ID declaration - file: # state declaration + file: # state module declaration - managed # function - source: salt://webserver/index.html # function arg - require: # requisite declaration @@ -72,7 +72,7 @@ want to install our custom HTML file. (**Note:** the default location that Apache serves may differ from the above on your OS or distro. ``/srv/www`` could also be a likely place to look.) -**Line 8** the :ref:`state-declaration`. This example uses the Salt :mod:`file +**Line 8** the :ref:`state-module-declaration`. This example uses the Salt :mod:`file state `. **Line 9** is the :ref:`function-declaration`. The :func:`managed function