Allow compound expressions to begin with "not"

This commit is contained in:
Thayne Harbaugh 2015-04-22 17:38:17 -06:00
parent 3867950e13
commit d6fe499f55
5 changed files with 69 additions and 58 deletions

View file

@ -41,18 +41,11 @@ That same example expressed in a :term:`top file` looks like the following:
- match: compound
- webserver
Note that a leading ``not`` is not supported in compound matches. Instead,
something like the following must be done:
.. code-block:: bash
salt -C '* and not G@kernel:Darwin' test.ping
Excluding a minion based on its ID is also possible:
.. code-block:: bash
salt -C '* and not web-dc1-srv' test.ping
salt -C 'not web-dc1-srv' test.ping
Precedence Matching
-------------------

View file

@ -2327,7 +2327,7 @@ def get_id(opts, cache_minion_id=False):
except (IOError, OSError):
pass
if '__role' in opts and opts.get('__role') == 'minion':
log.debug('Guessing ID. The id can be explicitly in set {0}'
log.debug('Guessing ID. The id can be explicitly set in {0}'
.format(os.path.join(salt.syspaths.CONFIG_DIR, 'minion')))
newid = salt.utils.network.generate_minion_id()

View file

@ -2311,6 +2311,7 @@ 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))
ref = {'G': 'grain',
'P': 'grain_pcre',
'I': 'pillar',
@ -2325,37 +2326,40 @@ class Matcher(object):
tokens = tgt.split()
for match in tokens:
# Try to match tokens from the compound target, first by using
# the 'G, X, I, L, S, E' matcher types, then by hostname glob.
# the 'G, X, I, J, L, S, E' matcher types, then by hostname glob.
if '@' in match and match[1] == '@':
comps = match.split('@')
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))(
'@'.join(comps[1:])
)
)
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
if results or match in ['(', ')']:
if results:
if results[-1] == '(' and match in ('and', 'or'):
log.error('Invalid beginning operator after "(": {0}'.format(match))
return False
if match == 'not':
match_suffix = results[-1]
if not (match_suffix == 'and' or match_suffix == 'or'):
if not results[-1] in ('and', 'or', '('):
results.append('and')
results.append(match)
else:
# seq start with oper, fail
if match not in ['(', ')']:
# seq start with binary oper, fail
if match not in ['(', 'not']:
log.error('Invalid beginning operator: {0}'.format(match))
return False
results.append(match)
else:
# The match is not explicitly defined, evaluate it as a glob
results.append(str(self.glob_match(match)))
results = ' '.join(results)
log.debug('compound_match "{0}" => "{1}"'.format(tgt, results))
try:
return eval(results) # pylint: disable=W0123
except Exception:

View file

@ -342,9 +342,12 @@ class CkMinions(object):
'''
Return the minions found by looking via compound matcher
'''
log.debug('_check_compound_minions({0})'.format(expr))
minions = set(
os.listdir(os.path.join(self.opts['pki_dir'], self.acc))
)
log.debug('minions: {0}'.format(minions))
if self.opts.get('minion_data_cache', False):
ref = {'G': self._check_grain_minions,
'P': self._check_grain_pcre_minions,
@ -365,30 +368,31 @@ class CkMinions(object):
# 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('@')
comps = match.split('@', 1)
matcher = ref.get(comps[0])
matcher_args = ['@'.join(comps[1:])]
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(str(set(matcher(*matcher_args))))
results.append(')')
unmatched.pop()
else:
results.append(str(set(matcher(*matcher_args))))
elif match in opers:
# We didn't match a target, so append a boolean operator or
# subexpression
if results:
if results[-1] == '(' and match in ('and', 'or'):
log.error('Invalid beginning operator after "(": {0}'.format(match))
return []
if match == 'not':
result_suffix = results[-1]
if not (result_suffix == '&' or result_suffix == '|'):
if not results[-1] in ('&', '|', '('):
results.append('&')
results.append('(')
results.append(str(set(minions)))
@ -418,10 +422,16 @@ class CkMinions(object):
return []
else:
# seq start with oper, fail
if match == '(':
if match == 'not':
results.append('(')
results.append(str(set(minions)))
results.append('-')
unmatched.append('-')
elif match == '(':
results.append(match)
unmatched.append(match)
else:
log.error('Expression cannot begin with binary operator: {0}'.format(match))
return []
else:
# The match is not explicitly defined, evaluate as a glob
@ -536,7 +546,7 @@ class CkMinions(object):
infinite.append('grain_pcre')
if '@' in valid and valid[1] == '@':
comps = valid.split('@')
comps = valid.split('@', 1)
v_matcher = ref.get(comps[0])
v_expr = comps[1]
else:

View file

@ -16,6 +16,10 @@ import integration
import salt.utils
def minion_in_returns(minion, lines):
return bool([True for line in lines if line == '{0}:'.format(minion)])
class MatchTest(integration.ShellCase, integration.ShellCaseCommonTestsMixIn):
'''
Test salt matchers
@ -40,58 +44,58 @@ class MatchTest(integration.ShellCase, integration.ShellCaseCommonTestsMixIn):
test salt compound matcher
'''
data = self.run_salt('-C "min* and G@test_grain:cheese" test.ping')
data = '\n'.join(data)
self.assertIn('minion', data)
self.assertNotIn('sub_minion', data)
self.assertTrue(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-C "min* and not G@test_grain:foo" test.ping')
data = '\n'.join(data)
self.assertIn('minion', data)
self.assertNotIn('sub_minion', data)
self.assertTrue(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-C "min* not G@test_grain:foo" test.ping')
data = '\n'.join(data)
self.assertIn('minion', data)
self.assertNotIn('sub_minion', data)
self.assertTrue(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
match = 'P@test_grain:^cheese$ and * and G@test_grain:cheese'
data = self.run_salt('-t 1 -C \'{0}\' test.ping'.format(match))
data = '\n'.join(data)
self.assertIn('minion', data)
self.assertNotIn('sub_minion', data)
self.assertTrue(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
match = 'L@sub_minion and E@.*'
data = self.run_salt('-t 1 -C "{0}" test.ping'.format(match))
data = '\n'.join(data)
self.assertIn('sub_minion', data)
self.assertNotIn('minion', data.replace('sub_minion', 'stub'))
self.assertTrue(minion_in_returns('sub_minion', data))
self.assertFalse(minion_in_returns('minion', data))
time.sleep(2)
data = self.run_salt("-C 'not sub_minion' 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 '* and ( not G@test_grain:cheese )' test.ping")
self.assertFalse(minion_in_returns('minion', data))
self.assertTrue(minion_in_returns('sub_minion', data))
def test_nodegroup(self):
'''
test salt nodegroup matcher
'''
def minion_in_target(minion, lines):
return sum([line == '{0}:'.format(minion) for line in lines])
data = self.run_salt('-N min test.ping')
self.assertTrue(minion_in_target('minion', data))
self.assertFalse(minion_in_target('sub_minion', data))
self.assertTrue(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-N sub_min test.ping')
self.assertFalse(minion_in_target('minion', data))
self.assertTrue(minion_in_target('sub_minion', data))
self.assertFalse(minion_in_returns('minion', data))
self.assertTrue(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-N mins test.ping')
self.assertTrue(minion_in_target('minion', data))
self.assertTrue(minion_in_target('sub_minion', data))
self.assertTrue(minion_in_returns('minion', data))
self.assertTrue(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-N unknown_nodegroup test.ping')
self.assertFalse(minion_in_target('minion', data))
self.assertFalse(minion_in_target('sub_minion', data))
self.assertFalse(minion_in_returns('minion', data))
self.assertFalse(minion_in_returns('sub_minion', data))
time.sleep(2)
data = self.run_salt('-N redundant_minions test.ping')
self.assertTrue(minion_in_target('minion', data))
self.assertTrue(minion_in_target('sub_minion', data))
self.assertTrue(minion_in_returns('minion', data))
self.assertTrue(minion_in_returns('sub_minion', data))
time.sleep(2)
data = '\n'.join(self.run_salt('-N nodegroup_loop_a test.ping'))
self.assertIn('No minions matched', data)