mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 17:50:20 +00:00
Add match runner
This commit is contained in:
parent
0cb3dc87e7
commit
0873ece26c
5 changed files with 372 additions and 0 deletions
1
changelog/63278.added
Normal file
1
changelog/63278.added
Normal file
|
@ -0,0 +1 @@
|
|||
Added match runner
|
|
@ -30,6 +30,7 @@ runner modules
|
|||
launchd
|
||||
lxc
|
||||
manage
|
||||
match
|
||||
mattermost
|
||||
mine
|
||||
nacl
|
||||
|
|
5
doc/ref/runners/all/salt.runners.match.rst
Normal file
5
doc/ref/runners/all/salt.runners.match.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
salt.runners.match
|
||||
===================
|
||||
|
||||
.. automodule:: salt.runners.match
|
||||
:members:
|
69
salt/runners/match.py
Normal file
69
salt/runners/match.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Run matchers from the master context.
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
"""
|
||||
import logging
|
||||
|
||||
import salt.utils.minions
|
||||
import salt.utils.verify
|
||||
from salt.defaults import DEFAULT_TARGET_DELIM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def compound_matches(expr, minion_id):
|
||||
"""
|
||||
Check whether a minion is matched by a given compound match expression.
|
||||
Pillar values will be matched literally only since this function is intended
|
||||
for remote calling. This also applies to node groups defined on the master.
|
||||
|
||||
.. note::
|
||||
If a module calls this runner from a minion, you will need to explicitly
|
||||
allow the remote call. See :conf_master:`peer_run`.
|
||||
|
||||
.. note::
|
||||
Custom matchers are not respected.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt-run match.compound_matches 'I@foo:bar and G@os:Deb* and not db*' myminion
|
||||
|
||||
expr
|
||||
The :term:`Compound Matcher` expression to validate against.
|
||||
|
||||
minion_id
|
||||
The minion ID of the minion to check the match for.
|
||||
|
||||
"""
|
||||
try:
|
||||
# Ensure that if the minion data cache is disabled, we always return
|
||||
# False. This is because the matcher will return a list of all minions
|
||||
# in that case (assumption is greedy).
|
||||
if not __opts__.get("minion_data_cache", True):
|
||||
log.warning(
|
||||
"Minion data cache is disabled. Cannot evaluate compound matcher expression."
|
||||
)
|
||||
return False
|
||||
# Ensure the passed minion ID is valid and exists.
|
||||
if not salt.utils.verify.valid_id(__opts__, minion_id):
|
||||
log.warning("Got invalid minion ID.")
|
||||
return False
|
||||
log.debug(f"Evaluating if minion '{minion_id}' is matched by '{expr}'.")
|
||||
ckminions = salt.utils.minions.CkMinions(__opts__)
|
||||
# Compound expressions are usually evaluated in greedy mode since you
|
||||
# want to make sure the executing user has privileges to run a command on
|
||||
# any possibly matching minion, including those with uncached data.
|
||||
# This function has the opposite requirements, we want to make absolutely
|
||||
# sure the minion is matched by the expression.
|
||||
# Thus we do not include minions whose data has not been cached (greedy=False).
|
||||
# Also, allow exact pillar matches only to make enumeration attacks harder.
|
||||
minions = ckminions._check_compound_pillar_exact_minions(
|
||||
expr, DEFAULT_TARGET_DELIM, greedy=False
|
||||
)
|
||||
return minion_id in minions["minions"]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
return False
|
296
tests/pytests/integration/runners/test_match.py
Normal file
296
tests/pytests/integration/runners/test_match.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
"""
|
||||
Integration tests for the match runner
|
||||
"""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.slow_test,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_master_config():
|
||||
return {
|
||||
"open_mode": True,
|
||||
"peer_run": {
|
||||
"match-minion-bob": [
|
||||
"match.compound_matches",
|
||||
],
|
||||
},
|
||||
"nodegroups": {
|
||||
"alice_eve": "I@name:ali*",
|
||||
"alice": "L@match-minion-alice",
|
||||
},
|
||||
"minion_data_cache": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def pillar_tree(match_salt_master, match_salt_minion_alice, match_salt_minion_eve):
|
||||
top_file = f"""
|
||||
base:
|
||||
'{match_salt_minion_alice.id}':
|
||||
- alice
|
||||
'{match_salt_minion_eve.id}':
|
||||
- eve
|
||||
"""
|
||||
alice_pillar_file = """
|
||||
name: alice
|
||||
"""
|
||||
eve_pillar_file = """
|
||||
name: alice_whoops_sorry_eve_hrhr
|
||||
"""
|
||||
top_tempfile = match_salt_master.pillar_tree.base.temp_file("top.sls", top_file)
|
||||
alice_tempfile = match_salt_master.pillar_tree.base.temp_file(
|
||||
"alice.sls", alice_pillar_file
|
||||
)
|
||||
eve_tempfile = match_salt_master.pillar_tree.base.temp_file(
|
||||
"eve.sls", eve_pillar_file
|
||||
)
|
||||
|
||||
with top_tempfile, alice_tempfile, eve_tempfile:
|
||||
ret = match_salt_minion_alice.salt_call_cli().run(
|
||||
"saltutil.refresh_pillar", wait=True
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is True
|
||||
ret = match_salt_minion_eve.salt_call_cli().run(
|
||||
"saltutil.refresh_pillar", wait=True
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is True
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_master(salt_factories, match_master_config):
|
||||
factory = salt_factories.salt_master_daemon(
|
||||
"match-master", defaults=match_master_config
|
||||
)
|
||||
with factory.started():
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_minion_alice(match_salt_master):
|
||||
assert match_salt_master.is_running()
|
||||
factory = match_salt_master.salt_minion_daemon(
|
||||
"match-minion-alice",
|
||||
defaults={"open_mode": True, "grains": {"role": "alice"}},
|
||||
)
|
||||
with factory.started():
|
||||
# Sync All
|
||||
salt_call_cli = factory.salt_call_cli()
|
||||
ret = salt_call_cli.run("saltutil.sync_all", _timeout=120)
|
||||
assert ret.returncode == 0, ret
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_minion_eve(match_salt_master):
|
||||
assert match_salt_master.is_running()
|
||||
factory = match_salt_master.salt_minion_daemon(
|
||||
"match-minion-eve",
|
||||
defaults={"open_mode": True, "grains": {"role": "eve"}},
|
||||
)
|
||||
with factory.started():
|
||||
# Sync All
|
||||
salt_call_cli = factory.salt_call_cli()
|
||||
ret = salt_call_cli.run("saltutil.sync_all", _timeout=120)
|
||||
assert ret.returncode == 0, ret
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_minion_bob(match_salt_master):
|
||||
assert match_salt_master.is_running()
|
||||
factory = match_salt_master.salt_minion_daemon(
|
||||
"match-minion-bob",
|
||||
defaults={"open_mode": True},
|
||||
)
|
||||
with factory.started():
|
||||
# Sync All
|
||||
salt_call_cli = factory.salt_call_cli()
|
||||
ret = salt_call_cli.run("saltutil.sync_all", _timeout=120)
|
||||
assert ret.returncode == 0, ret
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_call_cli(match_salt_minion_bob):
|
||||
return match_salt_minion_bob.salt_call_cli()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def match_salt_run_cli(match_salt_master):
|
||||
return match_salt_master.salt_run_cli()
|
||||
|
||||
|
||||
class TestMatchCompoundRunner:
|
||||
@pytest.fixture
|
||||
def alice_uncached(self, match_salt_minion_alice, match_salt_run_cli):
|
||||
ret = match_salt_run_cli.run("cache.clear_all", "match-minion-alice")
|
||||
assert ret.returncode == 0
|
||||
yield
|
||||
ret = match_salt_minion_alice.salt_call_cli().run("pillar.items")
|
||||
|
||||
@pytest.fixture
|
||||
def eve_cached(self, match_salt_minion_eve):
|
||||
ret = match_salt_minion_eve.salt_call_cli().run("pillar.items")
|
||||
assert ret.returncode == 0
|
||||
yield
|
||||
|
||||
@pytest.fixture
|
||||
def alice_down(self, match_salt_minion_alice):
|
||||
with match_salt_minion_alice.stopped():
|
||||
yield
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,expected",
|
||||
[
|
||||
("G@role:alice", True),
|
||||
("G@role:ali*", True),
|
||||
(r"E@match\-minion\-(alice|bob)", True),
|
||||
("P@role:^(alice|bob)$", True),
|
||||
("L@match-minion-alice,match-minion-bob", True),
|
||||
("I@name:alice", True),
|
||||
("I@name:ali*", False),
|
||||
("J@name:alice", True),
|
||||
("J@name:^(alice|bob)$", False),
|
||||
("N@alice", True),
|
||||
("N@alice_eve", False),
|
||||
("G@role:ali* and I@name:alice", True),
|
||||
("G@role:ali* and I@name:ali*", False),
|
||||
("G@role:ali* or I@name:ali*", True),
|
||||
("G@role:ali* and not I@name:alice", False),
|
||||
],
|
||||
)
|
||||
def test_match_compound_matches(self, match_salt_run_cli, expr, expected):
|
||||
ret = match_salt_run_cli.run(
|
||||
"match.compound_matches", expr, "match-minion-alice"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is expected
|
||||
|
||||
@pytest.mark.usefixtures("eve_cached")
|
||||
def test_match_compound_matches_only_allows_exact_pillar_matching(
|
||||
self, match_salt_run_cli
|
||||
):
|
||||
"""
|
||||
This check is mostly redundant, but better check explicitly with the scenario
|
||||
to prevent because it is security-critical.
|
||||
"""
|
||||
ret = match_salt_run_cli.run(
|
||||
"match.compound_matches", "I@name:alic*", "match-minion-eve"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,expected",
|
||||
[
|
||||
("G@role:alice", False),
|
||||
("G@role:ali*", False),
|
||||
("I@name:alice", False),
|
||||
("I@name:ali*", False),
|
||||
("G@role:ali* and I@name:alice", False),
|
||||
("L@match-minion-alice,match-minion-bob", True),
|
||||
("L@match-minion-alice,match-minion-bob and G@role:alice", False),
|
||||
("L@match-minion-alice,match-minion-bob and I@name:alice", False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("alice_uncached")
|
||||
def test_match_compound_matches_with_uncached_minion_data(
|
||||
self, match_salt_run_cli, expr, expected
|
||||
):
|
||||
ret = match_salt_run_cli.run(
|
||||
"match.compound_matches", expr, "match-minion-alice"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,expected",
|
||||
[
|
||||
("G@role:alice", True),
|
||||
("G@role:ali*", True),
|
||||
("I@name:alice", True),
|
||||
("I@name:ali*", False),
|
||||
("G@role:ali* and I@name:alice", True),
|
||||
("L@match-minion-alice,match-minion-bob", True),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("alice_down")
|
||||
def test_match_compound_matches_when_minion_is_down(
|
||||
self, match_salt_run_cli, expr, expected
|
||||
):
|
||||
ret = match_salt_run_cli.run(
|
||||
"match.compound_matches", expr, "match-minion-alice"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"minion_id",
|
||||
[
|
||||
"hi\\there",
|
||||
"my/minion",
|
||||
"nonexistent",
|
||||
],
|
||||
)
|
||||
def test_match_compound_matches_with_invalid_minion_id(
|
||||
self, minion_id, match_salt_run_cli
|
||||
):
|
||||
ret = match_salt_run_cli.run("match.compound_matches", "*", minion_id)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,expected",
|
||||
[
|
||||
("G@role:alice", True),
|
||||
("G@role:ali*", True),
|
||||
("I@name:alice", True),
|
||||
("I@name:ali*", False),
|
||||
("G@role:ali* and I@name:alice", True),
|
||||
("L@match-minion-alice,match-minion-bob", True),
|
||||
],
|
||||
)
|
||||
def test_match_compound_matches_as_peer_run(
|
||||
self, match_salt_call_cli, expr, expected
|
||||
):
|
||||
ret = match_salt_call_cli.run(
|
||||
"publish.runner", "match.compound_matches", [expr, "match-minion-alice"]
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is expected
|
||||
|
||||
|
||||
class TestMatchCompoundRunnerWithoutMinionDataCache:
|
||||
@pytest.fixture(scope="class")
|
||||
def match_master_config(self):
|
||||
return {
|
||||
"open_mode": True,
|
||||
"minion_data_cache": False,
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr",
|
||||
[
|
||||
"G@role:alice",
|
||||
"G@role:ali*",
|
||||
"I@name:alice",
|
||||
"I@name:ali*",
|
||||
"G@role:ali* and I@name:alice",
|
||||
],
|
||||
)
|
||||
def test_match_compound_matches(self, match_salt_run_cli, expr):
|
||||
ret = match_salt_run_cli.run(
|
||||
"match.compound_matches", expr, "match-minion-alice"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is False
|
Loading…
Add table
Reference in a new issue