Support master tops on masterless minions

Also, make salt-ssh master tops behave like regular ones, i.e. merge the
returns of multiple master top modules for the same environment.
This commit is contained in:
jeanluc 2023-10-28 22:04:34 +02:00 committed by Pedro Algarvio
parent d7f20320d5
commit d7338a0e79
9 changed files with 162 additions and 36 deletions

1
changelog/65479.added.md Normal file
View file

@ -0,0 +1 @@
Added support for master top modules on masterless minions

1
changelog/65480.fixed.md Normal file
View file

@ -0,0 +1 @@
Made salt-ssh merge master top returns for the same environment

View file

@ -12,6 +12,10 @@ The old `external_nodes` option has been removed. The master tops system
provides a pluggable and extendable replacement for it, allowing for multiple
different subsystems to provide top file data.
.. versionchanged:: 3007.0
Masterless minions now support master top modules as well.
Using the new `master_tops` option is simple:
.. code-block:: yaml

View file

@ -74,7 +74,6 @@ class SSHHighState(salt.state.BaseHighState):
salt.state.BaseHighState.__init__(self, opts)
self.state = SSHState(opts, pillar, wrapper, context=context)
self.matchers = salt.loader.matchers(self.opts)
self.tops = salt.loader.tops(self.opts)
self._pydsl_all_decls = {}
self._pydsl_render_stack = []
@ -92,32 +91,7 @@ class SSHHighState(salt.state.BaseHighState):
"""
Evaluate master_tops locally
"""
if "id" not in self.opts:
log.error("Received call for external nodes without an id")
return {}
if not salt.utils.verify.valid_id(self.opts, self.opts["id"]):
return {}
# Evaluate all configured master_tops interfaces
grains = {}
ret = {}
if "grains" in self.opts:
grains = self.opts["grains"]
for fun in self.tops:
if fun not in self.opts.get("master_tops", {}):
continue
try:
ret.update(self.tops[fun](opts=self.opts, grains=grains))
except Exception as exc: # pylint: disable=broad-except
# If anything happens in the top generation, log it and move on
log.error(
"Top function %s failed with error %s for minion %s",
fun,
exc,
self.opts["id"],
)
return ret
return self._local_master_tops()
def destroy(self):
if self.client:

View file

@ -70,13 +70,15 @@ __proxyenabled__ = ["*"]
log = logging.getLogger(__name__)
TOP_ENVS_CKEY = "saltutil._top_file_envs"
def _get_top_file_envs():
"""
Get all environments from the top file
"""
try:
return __context__["saltutil._top_file_envs"]
return __context__[TOP_ENVS_CKEY]
except KeyError:
with salt.state.HighState(__opts__, initial_pillar=__pillar__.value()) as st_:
try:
@ -87,7 +89,7 @@ def _get_top_file_envs():
envs = "base"
except SaltRenderError as exc:
raise CommandExecutionError(f"Unable to render top file(s): {exc}")
__context__["saltutil._top_file_envs"] = envs
__context__[TOP_ENVS_CKEY] = envs
return envs
@ -244,10 +246,6 @@ def sync_sdb(saltenv=None, extmod_whitelist=None, extmod_blacklist=None):
<states-top>` will be checked for sdb modules to sync. If no top files
are found, then the ``base`` environment will be synced.
refresh : False
This argument has no affect and is included for consistency with the
other sync functions.
extmod_whitelist : None
comma-separated list of modules to sync
@ -473,8 +471,7 @@ def sync_renderers(
refresh : True
If ``True``, refresh the available execution modules on the minion.
This refresh will be performed even if no new renderers are synced.
Set to ``False`` to prevent this refresh. Set to ``False`` to prevent
this refresh.
Set to ``False`` to prevent this refresh.
extmod_whitelist : None
comma-separated list of modules to sync
@ -973,6 +970,57 @@ def sync_pillar(
return ret
def sync_tops(
saltenv=None,
refresh=True,
extmod_whitelist=None,
extmod_blacklist=None,
):
"""
.. versionadded:: 3007.0
Sync master tops from ``salt://_tops`` to the minion.
saltenv
The fileserver environment from which to sync. To sync from more than
one environment, pass a comma-separated list.
If not passed, then all environments configured in the :ref:`top files
<states-top>` will be checked for master tops to sync. If no top files
are found, then the ``base`` environment will be synced.
refresh : True
Refresh this module's cache containing the environments from which
extension modules are synced when ``saltenv`` is not specified.
This refresh will be performed even if no new master tops are synced.
Set to ``False`` to prevent this refresh.
extmod_whitelist : None
comma-separated list of modules to sync
extmod_blacklist : None
comma-separated list of modules to blacklist based on type
.. note::
This function will raise an error if executed on a traditional (i.e.
not masterless) minion
CLI Examples:
.. code-block:: bash
salt '*' saltutil.sync_tops
salt '*' saltutil.sync_tops saltenv=dev
"""
if __opts__["file_client"] != "local":
raise CommandExecutionError(
"Master top modules can only be synced to masterless minions"
)
if refresh:
__context__.pop(TOP_ENVS_CKEY, None)
return _sync("tops", saltenv, extmod_whitelist, extmod_blacklist)
def sync_executors(
saltenv=None, refresh=True, extmod_whitelist=None, extmod_blacklist=None
):
@ -1071,6 +1119,13 @@ def sync_all(
clean_pillar_cache=False,
):
"""
.. versionchanged:: 3007.0
On masterless minions, master top modules are now synced as well.
When ``refresh`` is set to ``True``, this module's cache containing
the environments from which extension modules are synced when
``saltenv`` is not specified will be refreshed.
.. versionchanged:: 2015.8.11,2016.3.2
On masterless minions, pillar modules are now synced, and refreshed
when ``refresh`` is set to ``True``.
@ -1081,7 +1136,9 @@ def sync_all(
refresh : True
Also refresh the execution modules and recompile pillar data available
to the minion. This refresh will be performed even if no new dynamic
to the minion. If this is a masterless minion, also refresh the environments
from which extension modules are synced after syncing master tops.
This refresh will be performed even if no new dynamic
modules are synced. Set to ``False`` to prevent this refresh.
.. important::
@ -1121,6 +1178,9 @@ def sync_all(
"""
log.debug("Syncing all")
ret = {}
if __opts__["file_client"] == "local":
# Sync tops first since this might influence the other syncs
ret["tops"] = sync_tops(saltenv, refresh, extmod_whitelist, extmod_blacklist)
ret["clouds"] = sync_clouds(saltenv, False, extmod_whitelist, extmod_blacklist)
ret["beacons"] = sync_beacons(saltenv, False, extmod_whitelist, extmod_blacklist)
ret["modules"] = sync_modules(saltenv, False, extmod_whitelist, extmod_blacklist)

View file

@ -43,6 +43,7 @@ import salt.utils.msgpack
import salt.utils.platform
import salt.utils.process
import salt.utils.url
import salt.utils.verify
# Explicit late import to avoid circular import. DO NOT MOVE THIS.
import salt.utils.yamlloader as yamlloader
@ -4243,8 +4244,43 @@ class BaseHighState:
Get results from the master_tops system. Override this function if the
execution of the master_tops needs customization.
"""
if self.opts.get("file_client", "remote") == "local":
return self._local_master_tops()
return self.client.master_tops()
def _local_master_tops(self):
# return early if we got nothing to do
if "master_tops" not in self.opts:
return {}
if "id" not in self.opts:
log.error("Received call for external nodes without an id")
return {}
if not salt.utils.verify.valid_id(self.opts, self.opts["id"]):
return {}
if getattr(self, "tops", None) is None:
self.tops = salt.loader.tops(self.opts)
grains = {}
ret = {}
if "grains" in self.opts:
grains = self.opts["grains"]
for fun in self.tops:
if fun not in self.opts["master_tops"]:
continue
try:
ret = salt.utils.dictupdate.merge(
ret, self.tops[fun](opts=self.opts, grains=grains), merge_lists=True
)
except Exception as exc: # pylint: disable=broad-except
# If anything happens in the top generation, log it and move on
log.error(
"Top function %s failed with error %s for minion %s",
fun,
exc,
self.opts["id"],
)
return ret
def load_dynamic(self, matches):
"""
If autoload_dynamic_modules is True then automatically load the

View file

@ -309,6 +309,20 @@ def sync_states(name, **kwargs):
return _sync_single(name, "states", **kwargs)
def sync_tops(name, **kwargs):
"""
Performs the same task as saltutil.sync_tops module
See :mod:`saltutil module for full list of options <salt.modules.saltutil>`
.. code-block:: yaml
sync_everything:
saltutil.sync_tops:
- refresh: True
"""
return _sync_single(name, "tops", **kwargs)
def sync_thorium(name, **kwargs):
"""
Performs the same task as saltutil.sync_thorium module

View file

@ -0,0 +1,33 @@
from pathlib import Path
import pytest
from tests.support.runtests import RUNTIME_VARS
@pytest.fixture(scope="module")
def minion_config_overrides():
return {"master_tops": {"master_tops_test": True}}
@pytest.fixture(scope="module", autouse=True)
def _master_tops_test(state_tree, loaders):
mod_contents = (
Path(RUNTIME_VARS.FILES) / "extension_modules" / "tops" / "master_tops_test.py"
).read_text()
try:
with pytest.helpers.temp_file(
"master_tops_test.py", mod_contents, state_tree / "_tops"
):
res = loaders.modules.saltutil.sync_tops()
assert "tops.master_tops_test" in res
yield
finally:
loaders.modules.saltutil.sync_tops()
def test_masterless_master_tops(loaders):
res = loaders.modules.state.show_top()
assert res
assert "base" in res
assert "master_tops_test" in res["base"]

View file

@ -37,6 +37,7 @@ def test_saltutil_sync_all_nochange():
"matchers": [],
"serializers": [],
"wrapper": [],
"tops": [],
}
state_id = "somename"
state_result = {
@ -73,6 +74,7 @@ def test_saltutil_sync_all_test():
"matchers": [],
"serializers": [],
"wrapper": [],
"tops": [],
}
state_id = "somename"
state_result = {
@ -110,6 +112,7 @@ def test_saltutil_sync_all_change():
"matchers": [],
"serializers": [],
"wrapper": [],
"tops": [],
}
state_id = "somename"
state_result = {