mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Allow grain and pillar delimiter to be specified in compound matcher syntax.
This commit is contained in:
parent
4d57a2d7c0
commit
993373655a
4 changed files with 174 additions and 92 deletions
|
@ -10,17 +10,19 @@ with CLI and :term:`top file` matching. To match using anything other than a
|
|||
glob, prefix the match string with the appropriate letter from the table below,
|
||||
followed by an ``@`` sign.
|
||||
|
||||
====== ==================== ===============================================================
|
||||
Letter Match Type Example
|
||||
====== ==================== ===============================================================
|
||||
G Grains glob ``G@os:Ubuntu``
|
||||
E PCRE Minion ID ``E@web\d+\.(dev|qa|prod)\.loc``
|
||||
P Grains PCRE ``P@os:(RedHat|Fedora|CentOS)``
|
||||
L List of minions ``L@minion1.example.com,minion3.domain.com or bl*.domain.com``
|
||||
I Pillar glob ``I@pdata:foobar``
|
||||
S Subnet/IP address ``S@192.168.1.0/24`` or ``S@192.168.1.100``
|
||||
R Range cluster ``R@%foo.bar``
|
||||
====== ==================== ===============================================================
|
||||
====== ========= ==================== ==============================================================
|
||||
Letter Delimiter Match Type Example
|
||||
====== ========= ==================== ==============================================================
|
||||
G x Grains glob ``G@os:Ubuntu``
|
||||
E PCRE Minion ID ``E@web\d+\.(dev|qa|prod)\.loc``
|
||||
P x Grains PCRE ``P@os:(RedHat|Fedora|CentOS)``
|
||||
L List of minions ``L@minion1.example.com,minion3.domain.com or bl*.domain.com``
|
||||
I x Pillar glob ``I@pdata:foobar``
|
||||
J x Pillar PCRE ``J@pdata:^(foo|bar)$``
|
||||
S Subnet/IP address ``S@192.168.1.0/24`` or ``S@192.168.1.100``
|
||||
R Range cluster ``R@%foo.bar``
|
||||
====== ========= ==================== ==============================================================
|
||||
|
||||
|
||||
Matchers can be joined using boolean ``and``, ``or``, and ``not`` operators.
|
||||
|
||||
|
@ -60,3 +62,18 @@ Matches can be grouped together with parentheses to explicitly declare precedenc
|
|||
|
||||
Be certain to note that spaces are required between the parentheses and targets. Failing to obey this
|
||||
rule may result in incorrect targeting!
|
||||
|
||||
Alternate Delimiters
|
||||
--------------------
|
||||
|
||||
.. versionadded:: Beryllium
|
||||
|
||||
Some matchers allow an optional delimiter character specified between the
|
||||
leading matcher character and the ``@`` pattern separator character. This
|
||||
can be essential when the globbing or PCRE pattern may use the default
|
||||
delimiter character ``:``. This avoids incorrect interpretation of the
|
||||
pattern as part of the grain or pillar data structure traversal.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt -C 'J|@foo|bar|^foo:bar$ or J!@gitrepo!https://github.com:example/project.git' test.ping
|
||||
|
|
|
@ -2311,55 +2311,72 @@ class Matcher(object):
|
|||
if not isinstance(tgt, six.string_types):
|
||||
log.debug('Compound target received that is not a string')
|
||||
return False
|
||||
log.debug('compound_match({0})'.format(tgt))
|
||||
log.debug('compound_match: {0} ? {1}'.format(tgt, self.opts['id']))
|
||||
ref = {'G': 'grain',
|
||||
'P': 'grain_pcre',
|
||||
'I': 'pillar',
|
||||
'J': 'pillar_pcre',
|
||||
'L': 'list',
|
||||
'N': None, # Nodegroups should already be expanded
|
||||
'S': 'ipcidr',
|
||||
'E': 'pcre'}
|
||||
if HAS_RANGE:
|
||||
ref['R'] = 'range'
|
||||
|
||||
results = []
|
||||
opers = ['and', 'or', 'not', '(', ')']
|
||||
tokens = tgt.split()
|
||||
for match in tokens:
|
||||
# Try to match tokens from the compound target, first by using
|
||||
# the 'G, X, I, J, L, S, E' matcher types, then by hostname glob.
|
||||
if '@' in match and match[1] == '@':
|
||||
comps = match.split('@', 1)
|
||||
matcher = ref.get(comps[0])
|
||||
if not matcher:
|
||||
# If an unknown matcher is called at any time, fail out
|
||||
log.error('Invalid matcher: {0}'.format(comps[0]))
|
||||
return False
|
||||
results.append(
|
||||
str(getattr(self, '{0}_match'.format(matcher))(comps[1]))
|
||||
)
|
||||
elif match in opers:
|
||||
# We didn't match a target, so append a boolean operator or
|
||||
# subexpression
|
||||
words = tgt.split()
|
||||
for word in words:
|
||||
target_info = salt.utils.minions.parse_target(word)
|
||||
|
||||
# Easy check first
|
||||
if word in opers:
|
||||
if results:
|
||||
if results[-1] == '(' and match in ('and', 'or'):
|
||||
log.error('Invalid beginning operator after "(": {0}'.format(match))
|
||||
if results[-1] == '(' and word in ('and', 'or'):
|
||||
log.error('Invalid beginning operator after "(": {0}'.format(word))
|
||||
return False
|
||||
if match == 'not':
|
||||
if word == 'not':
|
||||
if not results[-1] in ('and', 'or', '('):
|
||||
results.append('and')
|
||||
results.append(match)
|
||||
results.append(word)
|
||||
else:
|
||||
# seq start with binary oper, fail
|
||||
if match not in ['(', 'not']:
|
||||
log.error('Invalid beginning operator: {0}'.format(match))
|
||||
if word not in ['(', 'not']:
|
||||
log.error('Invalid beginning operator: {0}'.format(word))
|
||||
return False
|
||||
results.append(match)
|
||||
results.append(word)
|
||||
|
||||
elif target_info and target_info.get('engine'):
|
||||
if 'N' == target_info['engine']:
|
||||
# Nodegroups should already be expanded/resolved to other engines
|
||||
log.error('Detected nodegroup expansion failure of "{0}"'.format(match))
|
||||
return False
|
||||
engine = ref.get(target_info['engine'])
|
||||
if not engine:
|
||||
# If an unknown engine is called at any time, fail out
|
||||
log.error('Unrecognized target engine "{0}" for'
|
||||
' target expression "{1}"'.format(
|
||||
target_info['engine'],
|
||||
word,
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
engine_args = [target_info['pattern']]
|
||||
engine_kwargs = {}
|
||||
if target_info.get('delimiter'):
|
||||
engine_kwargs['delimiter'] = target_info.get['delimiter']
|
||||
|
||||
results.append(
|
||||
str(getattr(self, '{0}_match'.format(engine))(*engine_args, **engine_kwargs))
|
||||
)
|
||||
|
||||
else:
|
||||
# The match is not explicitly defined, evaluate it as a glob
|
||||
results.append(str(self.glob_match(match)))
|
||||
results.append(str(self.glob_match(word)))
|
||||
|
||||
results = ' '.join(results)
|
||||
log.debug('compound_match "{0}" => "{1}"'.format(tgt, results))
|
||||
log.debug('compound_match {0} ? "{1}" => "{2}"'.format(self.opts['id'], tgt, results))
|
||||
try:
|
||||
return eval(results) # pylint: disable=W0123
|
||||
except Exception:
|
||||
|
|
|
@ -29,6 +29,24 @@ except ImportError:
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
TARGET_REX = re.compile(
|
||||
r'''(?x)
|
||||
(
|
||||
(?P<engine>G|P|I|J|L|N|S|E|R) # Possible target engines
|
||||
(?P<delimiter>(?<=G|P|I|J).)? # Optional delimiter for specific engines
|
||||
@)? # Engine+delimiter are separated by a '@'
|
||||
# character and are optional for the target
|
||||
(?P<pattern>.+)$''' # The pattern passed to the target engine
|
||||
)
|
||||
|
||||
|
||||
def parse_target(target_expression):
|
||||
'''Parse `target_expressing` splitting it into `engine`, `delimiter`,
|
||||
`pattern` - returns a dict'''
|
||||
|
||||
match = TARGET_REX.match(target_expression)
|
||||
return match.groupdict() if match else None
|
||||
|
||||
|
||||
def get_minion_data(minion, opts):
|
||||
'''
|
||||
|
@ -196,6 +214,7 @@ class CkMinions(object):
|
|||
).get(search_type)
|
||||
if not salt.utils.subdict_match(search_results,
|
||||
expr,
|
||||
delimiter=delimiter,
|
||||
regex_match=regex_match,
|
||||
exact_match=exact_match) and id_ in minions:
|
||||
minions.remove(id_)
|
||||
|
@ -342,7 +361,7 @@ class CkMinions(object):
|
|||
'''
|
||||
Return the minions found by looking via compound matcher
|
||||
'''
|
||||
log.debug('_check_compound_minions({0})'.format(expr))
|
||||
log.debug('_check_compound_minions({0}, {1}, {2}, {3})'.format(expr, delimiter, greedy, pillar_exact))
|
||||
minions = set(
|
||||
os.listdir(os.path.join(self.opts['pki_dir'], self.acc))
|
||||
)
|
||||
|
@ -354,64 +373,48 @@ class CkMinions(object):
|
|||
'I': self._check_pillar_minions,
|
||||
'J': self._check_pillar_pcre_minions,
|
||||
'L': self._check_list_minions,
|
||||
'N': None, # nodegroups should already be expanded
|
||||
'S': self._check_ipcidr_minions,
|
||||
'E': self._check_pcre_minions,
|
||||
'R': self._all_minions}
|
||||
if pillar_exact:
|
||||
ref['I'] = self._check_pillar_exact_minions
|
||||
ref['J'] = self._check_pillar_exact_minions
|
||||
|
||||
results = []
|
||||
unmatched = []
|
||||
opers = ['and', 'or', 'not', '(', ')']
|
||||
tokens = expr.split()
|
||||
for match in tokens:
|
||||
# Try to match tokens from the compound target, first by using
|
||||
# the 'G, X, I, J, L, S, E' matcher types, then by hostname glob.
|
||||
if '@' in match and match[1] == '@':
|
||||
comps = match.split('@', 1)
|
||||
matcher = ref.get(comps[0])
|
||||
words = expr.split()
|
||||
for word in words:
|
||||
target_info = parse_target(word)
|
||||
|
||||
matcher_args = [comps[1]]
|
||||
if comps[0] in ('G', 'P', 'I', 'J'):
|
||||
matcher_args.append(delimiter)
|
||||
matcher_args.append(True)
|
||||
|
||||
if not matcher:
|
||||
# If an unknown matcher is called at any time, fail out
|
||||
log.error('Unknown matcher: {0}'.format(comps[0]))
|
||||
return []
|
||||
results.append(str(set(matcher(*matcher_args))))
|
||||
if unmatched and unmatched[-1] == '-':
|
||||
results.append(')')
|
||||
unmatched.pop()
|
||||
elif match in opers:
|
||||
# We didn't match a target, so append a boolean operator or
|
||||
# subexpression
|
||||
# Easy check first
|
||||
if word in opers:
|
||||
if results:
|
||||
if results[-1] == '(' and match in ('and', 'or'):
|
||||
log.error('Invalid beginning operator after "(": {0}'.format(match))
|
||||
if results[-1] == '(' and word in ('and', 'or'):
|
||||
log.error('Invalid beginning operator after "(": {0}'.format(word))
|
||||
return []
|
||||
if match == 'not':
|
||||
if word == 'not':
|
||||
if not results[-1] in ('&', '|', '('):
|
||||
results.append('&')
|
||||
results.append('(')
|
||||
results.append(str(set(minions)))
|
||||
results.append('-')
|
||||
unmatched.append('-')
|
||||
elif match == 'and':
|
||||
elif word == 'and':
|
||||
results.append('&')
|
||||
elif match == 'or':
|
||||
elif word == 'or':
|
||||
results.append('|')
|
||||
elif match == '(':
|
||||
results.append(match)
|
||||
unmatched.append(match)
|
||||
elif match == ')':
|
||||
elif word == '(':
|
||||
results.append(word)
|
||||
unmatched.append(word)
|
||||
elif word == ')':
|
||||
if not unmatched or unmatched[-1] != '(':
|
||||
log.error('Invalid compound expr (unexpected '
|
||||
'right parenthesis): {0}'
|
||||
.format(expr))
|
||||
return []
|
||||
results.append(match)
|
||||
results.append(word)
|
||||
unmatched.pop()
|
||||
if unmatched and unmatched[-1] == '-':
|
||||
results.append(')')
|
||||
|
@ -422,27 +425,55 @@ class CkMinions(object):
|
|||
return []
|
||||
else:
|
||||
# seq start with oper, fail
|
||||
if match == 'not':
|
||||
if word == 'not':
|
||||
results.append('(')
|
||||
results.append(str(set(minions)))
|
||||
results.append('-')
|
||||
unmatched.append('-')
|
||||
elif match == '(':
|
||||
results.append(match)
|
||||
unmatched.append(match)
|
||||
elif word == '(':
|
||||
results.append(word)
|
||||
unmatched.append(word)
|
||||
else:
|
||||
log.error('Expression cannot begin with binary operator: {0}'.format(match))
|
||||
log.error(
|
||||
'Expression may begin with'
|
||||
' binary operator: {0}'.format(word)
|
||||
)
|
||||
return []
|
||||
else:
|
||||
# The match is not explicitly defined, evaluate as a glob
|
||||
|
||||
elif target_info and target_info.get('engine'):
|
||||
if 'N' == target_info['engine']:
|
||||
# Nodegroups should already be expanded/resolved to other engines
|
||||
log.error('Detected nodegroup expansion failure of "{0}"'.format(word))
|
||||
return []
|
||||
engine = ref.get(target_info['engine'])
|
||||
if not engine:
|
||||
# If an unknown engine is called at any time, fail out
|
||||
log.error(
|
||||
'Unrecognized target engine "{0}" for'
|
||||
' target expression "{1}"'.format(
|
||||
target_info['engine'],
|
||||
word,
|
||||
)
|
||||
)
|
||||
return []
|
||||
|
||||
engine_args = [target_info.get('pattern')]
|
||||
if target_info['engine'] in ('G', 'P', 'I', 'J'):
|
||||
engine_args.append(target_info.get('delimiter') or ':')
|
||||
engine_args.append(True)
|
||||
|
||||
results.append(str(set(engine(*engine_args))))
|
||||
if unmatched and unmatched[-1] == '-':
|
||||
results.append(
|
||||
str(set(self._check_glob_minions(match, True))))
|
||||
results.append(')')
|
||||
unmatched.pop()
|
||||
else:
|
||||
results.append(
|
||||
str(set(self._check_glob_minions(match, True))))
|
||||
|
||||
else:
|
||||
# The match is not explicitly defined, evaluate as a glob
|
||||
results.append(str(set(self._check_glob_minions(word, True))))
|
||||
if unmatched and unmatched[-1] == '-':
|
||||
results.append(')')
|
||||
unmatched.pop()
|
||||
|
||||
for token in unmatched:
|
||||
results.append(')')
|
||||
results = ' '.join(results)
|
||||
|
@ -535,7 +566,8 @@ class CkMinions(object):
|
|||
'L': 'list',
|
||||
'S': 'ipcidr',
|
||||
'E': 'pcre',
|
||||
'N': 'node'}
|
||||
'N': 'node',
|
||||
None: 'glob'}
|
||||
infinite = [
|
||||
'node',
|
||||
'ipcidr',
|
||||
|
@ -545,13 +577,13 @@ class CkMinions(object):
|
|||
infinite.append('grain')
|
||||
infinite.append('grain_pcre')
|
||||
|
||||
if '@' in valid and valid[1] == '@':
|
||||
comps = valid.split('@', 1)
|
||||
v_matcher = ref.get(comps[0])
|
||||
v_expr = comps[1]
|
||||
else:
|
||||
v_matcher = 'glob'
|
||||
v_expr = valid
|
||||
target_info = parse_target(valid)
|
||||
if not target_info:
|
||||
log.error('Failed to parse valid target "{0}"'.format(valid))
|
||||
|
||||
v_matcher = ref.get(target_info.get('engine'))
|
||||
v_expr = target_info.get('pattern')
|
||||
|
||||
if v_matcher in infinite:
|
||||
# We can't be sure what the subset is, only match the identical
|
||||
# target
|
||||
|
|
|
@ -72,6 +72,22 @@ class MatchTest(integration.ShellCase, integration.ShellCaseCommonTestsMixIn):
|
|||
data = self.run_salt("-C '* and ( not G@test_grain:cheese )' test.ping")
|
||||
self.assertFalse(minion_in_returns('minion', data))
|
||||
self.assertTrue(minion_in_returns('sub_minion', data))
|
||||
time.sleep(2)
|
||||
data = self.run_salt("-C 'G%@planets%merc*' test.ping")
|
||||
self.assertTrue(minion_in_returns('minion', data))
|
||||
self.assertFalse(minion_in_returns('sub_minion', data))
|
||||
time.sleep(2)
|
||||
data = self.run_salt("-C 'P%@planets%^(mercury|saturn)$' test.ping")
|
||||
self.assertTrue(minion_in_returns('minion', data))
|
||||
self.assertTrue(minion_in_returns('sub_minion', data))
|
||||
time.sleep(2)
|
||||
data = self.run_salt("-C 'I%@companions%three%sarah*' test.ping")
|
||||
self.assertTrue(minion_in_returns('minion', data))
|
||||
self.assertTrue(minion_in_returns('sub_minion', data))
|
||||
time.sleep(2)
|
||||
data = self.run_salt("-C 'J%@knights%^(Lancelot|Galahad)$' test.ping")
|
||||
self.assertTrue(minion_in_returns('minion', data))
|
||||
self.assertTrue(minion_in_returns('sub_minion', data))
|
||||
|
||||
def test_nodegroup(self):
|
||||
'''
|
||||
|
|
Loading…
Add table
Reference in a new issue