From a85ffa8f8b11484d906b4501acc3dd10d241ebe0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 Mar 2015 14:47:38 -0400 Subject: [PATCH] Add regex names and attribute match to noderange Enhance noderange to implement: attribute.name==value (also attribute.name=value) -- strict equality match attribute.name=~value -- regular expression match attribute.name!=value -- strict negative match attribute.name!~value -- negative regular expression match ~nameexpression -- search by node name using regular expression This also goes a step further by intelligently limiting searches when found to the right of @ or ,-. --- .../confluent/config/configmanager.py | 72 ++++++++++++++++++- confluent_server/confluent/core.py | 1 - confluent_server/confluent/noderange.py | 23 +++--- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 6becdda1..0b690b14 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -444,6 +444,76 @@ class ConfigManager(object): self._cfgstore['nodes'] = {} self._bg_sync_to_file() + def filter_node_attributes(self, expression, nodes=None): + """Filtered nodelist according to expression + + expression may be: + attribute.name=value + attribute.name==value + attribute.name=~value + attribute.name!=value + attribute.name!~value + + == and != do strict equality. The ~ operators do a regular expression. + ! negates the sense of the match + + :param expression: The expression containing the criteria to match + :param nodes: Optional iterable set of nodes to limit the check + """ + exmatch = None + yieldmatches = True + if nodes is None: + nodes = self._cfgstore['nodes'] + if '==' in expression: + attribute, match = expression.split('==') + elif '!=' in expression: + attribute, match = expression.split('!=') + yieldmatches = False + elif '=~' in expression: + attribute, match = expression.split('=~') + exmatch = re.compile(match) + elif '!~' in expression: + attribute, match = expression.split('!~') + exmatch = re.compile(match) + yieldmatches = False + elif '=' in expression: + attribute, match = expression.split('=') + else: + raise Exception('Invalid Expression') + for node in nodes: + try: + currval = self._cfgstore['nodes'][node][attribute]['value'] + except KeyError: + # Let's treat 'not set' as being an empty string for this path + currval = '' + if exmatch: + if yieldmatches: + if exmatch.search(currval): + yield node + else: + if not exmatch.search(currval): + yield node + else: + if yieldmatches: + if match == currval: + yield node + else: + if match != currval: + yield node + + def filter_nodenames(self, expression, nodes=None): + """Filter nodenames by regular expression + + :param expression: Regular expression for matching nodenames + :param nodes: Optional iterable of candidates + """ + if nodes is None: + nodes = self._cfgstore['nodes'] + expression = re.compile(expression) + for node in nodes: + if expression.search(node): + yield node + def watch_attributes(self, nodes, attributes, callback): """ Watch a list of attributes for changes on a list of nodes @@ -680,7 +750,7 @@ class ConfigManager(object): cfgnodeobj = self._cfgstore['nodes'][node] nodeobj = {} if len(attributes) == 0: - relattribs = cfgnodeobj.iterkeys() + relattribs = cfgnodeobj for attribute in relattribs: if attribute.startswith('_'): # skip private things diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 7328bfc1..360ec4ad 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -269,7 +269,6 @@ def enumerate_node_collection(collectionpath, configmanager): collection = nested_lookup(noderesources, collectionpath[2:]) if len(collectionpath) == 2 and collectionpath[0] == 'noderange': collection['nodes'] = {} - print repr(collection) if not isinstance(collection, dict): raise exc.NotFoundException("Invalid element requested") return iterate_resources(collection) diff --git a/confluent_server/confluent/noderange.py b/confluent_server/confluent/noderange.py index b67eced4..84532174 100644 --- a/confluent_server/confluent/noderange.py +++ b/confluent_server/confluent/noderange.py @@ -24,7 +24,7 @@ import itertools import pyparsing as pp # construct custom grammar with pyparsing -_nodeword = pp.Word(pp.alphanums + '/=-:.*+') +_nodeword = pp.Word(pp.alphanums + '~^$/=-:.*+!') _nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']', unquoteResults=False) _nodeatom = pp.Group(pp.OneOrMore(_nodeword | _nodebracket)) @@ -51,11 +51,11 @@ class NodeRange(object): def nodes(self): return self._noderange - def _evaluate(self, parsetree): + def _evaluate(self, parsetree, filternodes=None): current_op = 0 # enum, 0 union, 1 subtract, 2 intersect current_range = set([]) if not isinstance(parsetree[0], list): # down to a plain text thing - return self._expandstring(parsetree) + return self._expandstring(parsetree, filternodes) for elem in parsetree: if elem == ',-': current_op = 1 @@ -66,9 +66,9 @@ class NodeRange(object): elif current_op == 0: current_range |= self._evaluate(elem) elif current_op == 1: - current_range -= self._evaluate(elem) + current_range -= self._evaluate(elem, current_range) elif current_op == 2: - current_range &= self._evaluate(elem) + current_range &= self._evaluate(elem, current_range) return current_range def failorreturn(self, atom): @@ -134,7 +134,7 @@ class NodeRange(object): grpcfg['noderange']['value'], self.cfm).nodes return nodes - def _expandstring(self, element): + def _expandstring(self, element, filternodes=None): prefix = '' for idx in xrange(len(element)): if element[idx][0] == '[': @@ -161,10 +161,15 @@ class NodeRange(object): return self.expandrange(element, '-') elif ':' in element: # : range for less ambiguity return self.expandrange(element, ':') - elif '=' in element: - raise Exception('TODO: criteria noderange') + elif '=' in element or '!~' in element: + if self.cfm is None: + raise Exception('Verification configmanager required') + return set(self.cfm.filter_node_attributes(element, filternodes)) elif element[0] in ('/', '~'): - raise Exception('TODO: regex noderange') + nameexpression = element[1:] + if self.cfm is None: + raise Exception('Verification configmanager required') + return set(self.cfm.filter_nodenames(nameexpression, filternodes)) elif '+' in element: raise Exception('TODO: plus range') if self.cfm is None: