Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/ref/states/requisites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ if any of the watched states changes.
In the example above, ``cmd.run`` will run only if there are changes in the
``file.managed`` state.

.. note::

When multiple state declarations share the same ID, ``onchanges`` still
resolves by the referenced state type and name. If a requisite resolves
back to the same state (self-reference), Salt ignores it to avoid
recursive requisites and logs a warning. Use distinct IDs if you need to
make ordering explicit or if name-based matching is ambiguous. If you
prefer ID-based matching, use the ``id`` requisite key explicitly to
avoid ambiguity between IDs and names. Running ``state.show_lowstate``
can help verify how a requisite resolves during compilation.

An easy mistake to make is using ``onchanges_in`` when ``onchanges`` is the
correct choice, as seen in this next example.

Expand Down
17 changes: 11 additions & 6 deletions salt/modules/cmdmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@

import salt.platform.win
from salt.utils.win_functions import escape_argument as _cmd_quote
from salt.utils.win_runas import runas as win_runas
import salt.utils.win_runas as win_runas

HAS_WIN_RUNAS = True
else:
Expand Down Expand Up @@ -788,7 +788,7 @@ def _run(
if change_windows_codepage:
salt.utils.win_chcp.set_codepage_id(windows_codepage)
try:
proc = win_runas(cmd, runas, password, **new_kwargs)
proc = win_runas.runas(cmd, runas, password, **new_kwargs)
except (OSError, pywintypes.error) as exc:
msg = "Unable to run command '{}' with the context '{}', reason: {}".format(
cmd if output_loglevel is not None else "REDACTED",
Expand Down Expand Up @@ -3023,16 +3023,21 @@ def _cleanup_tempfile(path):

win_cwd = False
if salt.utils.platform.is_windows() and runas:
# Let's make sure the user exists first
if not __salt__["user.info"](runas):
# Resolve the user for domain/UPN support before creating the temp dir
try:
resolved_runas = win_runas.resolve_logon_credentials(runas)
except CommandExecutionError as exc:
msg = f"Invalid user: {runas}"
raise CommandExecutionError(msg)
raise CommandExecutionError(msg) from exc

if cwd is None:
# Create a temp working directory
cwd = tempfile.mkdtemp(dir=__opts__["cachedir"])
win_cwd = True
salt.utils.win_dacl.set_permissions(
obj_name=cwd, principal=runas, permissions="full_control"
obj_name=cwd,
principal=resolved_runas.get("sam_name") or runas,
permissions="full_control",
)

(_, ext) = os.path.splitext(salt.utils.url.split_env(source)[0])
Expand Down
42 changes: 34 additions & 8 deletions salt/modules/cp.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,39 @@ def _render(contents):
return (path, dest)


def _normalize_template_context_overrides(context):
"""
Normalize a user-supplied template context without adding missing keys.
"""
normalized = dict(context)
for key in ("salt", "opts", "grains", "pillar"):
if key not in normalized:
continue
value = normalized.get(key)
if isinstance(value, NamedLoaderContext):
value = value.value()
if value is None:
value = {}
normalized[key] = value
return normalized


def _prepare_template_kwargs(kwargs):
"""
Ensure template rendering kwargs include the standard context keys.
"""
prepared = {} if not kwargs else dict(kwargs)
prepared.setdefault("salt", __salt__)
prepared.setdefault("pillar", __pillar__)
prepared.setdefault("grains", __grains__)
prepared.setdefault("opts", __opts__)
prepared = salt.utils.templates.normalize_render_context(prepared)
context = prepared.get("context")
if isinstance(context, dict):
prepared["context"] = _normalize_template_context_overrides(context)
return prepared


def get_file(
path, dest, saltenv=None, makedirs=False, template=None, gzip=None, **kwargs
):
Expand Down Expand Up @@ -329,14 +362,7 @@ def get_template(path, dest, template="jinja", saltenv=None, makedirs=False, **k
if not saltenv:
saltenv = __opts__["saltenv"] or "base"

if "salt" not in kwargs:
kwargs["salt"] = __salt__
if "pillar" not in kwargs:
kwargs["pillar"] = __pillar__
if "grains" not in kwargs:
kwargs["grains"] = __grains__
if "opts" not in kwargs:
kwargs["opts"] = __opts__
kwargs = _prepare_template_kwargs(kwargs)
with _client() as client:
return client.get_template(path, dest, template, makedirs, saltenv, **kwargs)

Expand Down
13 changes: 13 additions & 0 deletions salt/modules/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,7 @@ def list_all_versions(
include_alpha=False,
include_beta=False,
include_rc=False,
pre_releases=False,
user=None,
cwd=None,
index_url=None,
Expand Down Expand Up @@ -1644,6 +1645,12 @@ def list_all_versions(
include_rc
Include release candidates versions in the list

pre_releases
Include all pre-release versions (alpha, beta, and release candidates)
in the list. When set to True, this overrides individual
``include_alpha``, ``include_beta``, and ``include_rc`` settings.
.. versionadded:: 3007.2

user
The user under which to run pip

Expand All @@ -1667,6 +1674,12 @@ def list_all_versions(
cwd = _pip_bin_env(cwd, bin_env)
cmd = _get_pip_bin(bin_env)

# If pre_releases is True, include all pre-release types
if pre_releases:
include_alpha = True
include_beta = True
include_rc = True

# Is the `pip index` command available
pip_version = version(bin_env=bin_env, cwd=cwd, user=user)
if salt.utils.versions.compare(ver1=pip_version, oper=">=", ver2="21.2"):
Expand Down
18 changes: 15 additions & 3 deletions salt/renderers/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
log = logging.getLogger(__name__)


def _resolve_salt_context():
"""
Resolve __salt__ to a dict for template rendering when loader context is missing.
"""
funcs = __salt__
if isinstance(__salt__, NamedLoaderContext):
resolved = __salt__.value()
if isinstance(resolved, dict):
funcs = resolved
elif resolved is None:
funcs = {}
return funcs


def _split_module_dicts():
"""
Create a copy of __salt__ dictionary with module.function and module[function]
Expand All @@ -24,9 +38,7 @@ def _split_module_dicts():

{{ salt.cmd.run('uptime') }}
"""
funcs = __salt__
if isinstance(__salt__, NamedLoaderContext) and isinstance(__salt__.value(), dict):
funcs = __salt__.value()
funcs = _resolve_salt_context()
if not isinstance(funcs, dict):
return funcs
mod_dict = dict(funcs)
Expand Down
9 changes: 9 additions & 0 deletions salt/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2686,6 +2686,7 @@ def _check_requisites(self, low: LowChunk, running: dict[str, dict[str, Any]]):
"""
reqs = {}
pending = False
prereq_run_dict = self.pre
for req_type, chunk in self.dependency_dag.get_dependencies(low):
reqs.setdefault(req_type, []).append(chunk)
fun_stats = set()
Expand All @@ -2701,6 +2702,14 @@ def _check_requisites(self, low: LowChunk, running: dict[str, dict[str, Any]]):
for chunk in chunks:
tag = _gen_tag(chunk)
run_dict_chunk = run_dict.get(tag)
if (
run_dict_chunk is None
and r_type_base == RequisiteType.ONCHANGES.value
and chunk.get("__prereq__")
):
# If the requisite only ran in prereq (test) mode, use that
# result for onchanges to avoid recursive unmet requisites.
run_dict_chunk = prereq_run_dict.get(tag)
if run_dict_chunk:
filtered_run_dict[tag] = run_dict_chunk
run_dict = filtered_run_dict
Expand Down
57 changes: 41 additions & 16 deletions salt/states/pip_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ def _check_if_installed(
index_url,
extra_index_url,
pip_list=False,
pre_releases=False,
**kwargs,
):
"""
Expand Down Expand Up @@ -281,23 +282,32 @@ def _check_if_installed(
return ret
if force_reinstall is False and upgrade:
# Check desired version (if any) against currently-installed
include_alpha = False
include_beta = False
include_rc = False
if any(version_spec):
for spec in version_spec:
if "a" in spec[1]:
include_alpha = True
if "b" in spec[1]:
include_beta = True
if "rc" in spec[1]:
include_rc = True
# If pre_releases is True, include all pre-release types
if pre_releases:
include_alpha = True
include_beta = True
include_rc = True
else:
include_alpha = False
include_beta = False
include_rc = False
# Only check version spec for pre-release indicators if pre_releases is False
if any(version_spec):
for spec in version_spec:
if "a" in spec[1]:
include_alpha = True
if "b" in spec[1]:
include_beta = True
if "rc" in spec[1]:
include_rc = True
# Use pre_releases parameter for cleaner API when available
available_versions = __salt__["pip.list_all_versions"](
prefix,
bin_env=bin_env,
include_alpha=include_alpha,
include_beta=include_beta,
include_rc=include_rc,
pre_releases=pre_releases,
include_alpha=include_alpha if not pre_releases else True,
include_beta=include_beta if not pre_releases else True,
include_rc=include_rc if not pre_releases else True,
user=user,
cwd=cwd,
index_url=index_url,
Expand All @@ -320,7 +330,19 @@ def _check_if_installed(
"requirements".format(prefix)
)
return ret
if _pep440_version_cmp(pip_list[prefix], desired_version) == 0:
# Compare installed version with desired version
# When pre_releases=True, desired_version may include pre-releases
version_cmp = _pep440_version_cmp(pip_list[prefix], desired_version)
if version_cmp == 0:
# Installed version matches desired version exactly
ret["result"] = True
ret["comment"] = "Python package {} was already installed".format(
state_pkg_name
)
return ret
elif version_cmp == 1:
# Installed version is newer than desired version
# This can happen with pre-releases - keep the newer installed version
ret["result"] = True
ret["comment"] = "Python package {} was already installed".format(
state_pkg_name
Expand Down Expand Up @@ -554,7 +576,9 @@ def installed(
Current working directory to run pip from

pre_releases
Include pre-releases in the available versions
Include pre-releases in the available versions. When used with
``upgrade=True``, this allows upgrading to pre-release versions even
if they are not explicitly specified in the version requirements.

cert
Provide a path to an alternate CA bundle
Expand Down Expand Up @@ -889,6 +913,7 @@ def prepro(pkg):
index_url,
extra_index_url,
pip_list,
pre_releases=pre_releases,
**kwargs,
)
# If _check_if_installed result is None, something went wrong with
Expand Down
48 changes: 44 additions & 4 deletions salt/utils/requisite.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,26 @@ def _chunk_str(self, chunk: LowChunk) -> str:
node_dict["NAME"] = chunk["name"]
return str(node_dict)

def _filter_self_reqs(
self,
node_tag: str,
req_tags: Iterable[str],
req_type: RequisiteType,
low: LowChunk,
req_key: str,
req_val: str,
) -> set[str]:
filtered = {tag for tag in req_tags if tag != node_tag}
if len(filtered) != len(req_tags):
log.warning(
"Ignoring %s requisite on %s that points to itself (%s: %s)",
req_type.value,
self._chunk_str(low),
req_key,
req_val,
)
return filtered

def add_chunk(self, low: LowChunk, allow_aggregate: bool) -> None:
node_id = _gen_tag(low)
self.dag.add_node(
Expand Down Expand Up @@ -269,12 +289,22 @@ def add_dependency(
for sls, req_tags in self.sls_to_nodes.items():
if fnmatch.fnmatch(sls, req_val):
found = True
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
req_tags = self._filter_self_reqs(
node_tag, req_tags, req_type, low, req_key, req_val
)
if req_tags:
self._add_reqs(
node_tag, has_prereq_node, req_type, req_tags
)
else:
node_tag = _gen_tag(low)
if req_tags := self.sls_to_nodes.get(req_val, []):
found = True
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
req_tags = self._filter_self_reqs(
node_tag, req_tags, req_type, low, req_key, req_val
)
if req_tags:
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
elif self._is_fnmatch_pattern(req_val):
# This iterates over every chunk to check
# if any match instead of doing a look up since
Expand All @@ -283,11 +313,21 @@ def add_dependency(
for (state_type, name_or_id), req_tags in self.nodes_lookup_map.items():
if req_key == state_type and (fnmatch.fnmatch(name_or_id, req_val)):
found = True
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
req_tags = self._filter_self_reqs(
node_tag, req_tags, req_type, low, req_key, req_val
)
if req_tags:
self._add_reqs(
node_tag, has_prereq_node, req_type, req_tags
)
elif req_tags := self.nodes_lookup_map.get((req_key, req_val)):
found = True
node_tag = _gen_tag(low)
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
req_tags = self._filter_self_reqs(
node_tag, req_tags, req_type, low, req_key, req_val
)
if req_tags:
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
return found

def add_requisites(self, low: LowChunk, disabled_reqs: Sequence[str]) -> str | None:
Expand Down
Loading