# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2014 IBM Corporation # Copyright 2015 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # this will implement noderange grammar # considered ast, but a number of things violate python grammar like [] in # the middle of strings and use of @ for anything is not in their syntax import itertools import pyparsing as pp import re # construct custom grammar with pyparsing _nodeword = pp.Word(pp.alphanums + '~^$/=-:.*+!') _nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']', unquoteResults=False) _nodeatom = pp.Group(pp.OneOrMore(_nodeword | _nodebracket)) _paginationstart = pp.Group(pp.Word('<', pp.nums)) _paginationend = pp.Group(pp.Word('>', pp.nums)) _grammar = _nodeatom | ',-' | ',' | '@' | _paginationstart | _paginationend _parser = pp.nestedExpr(content=_grammar) _numextractor = pp.OneOrMore(pp.Word(pp.alphas + '-') | pp.Word(pp.nums)) numregex = re.compile('([0-9]+)') def humanify_nodename(nodename): """Analyzes nodename in a human way to enable natural sort :param nodename: The node name to analyze :returns: A structure that can be consumed by 'sorted' """ return [int(text) if text.isdigit() else text.lower() for text in re.split(numregex, nodename)] # TODO: pagination operators pp.nums for begin and end respective class NodeRange(object): """Iterate over a noderange :param noderange: string representing a noderange to evaluate :param config: Config manager object to use to vet elements """ def __init__(self, noderange, config=None): self.beginpage = None self.endpage = None self.cfm = config try: elements = _parser.parseString("(" + noderange + ")").asList()[0] except pp.ParseException as pe: raise Exception("Invalid syntax") if noderange[0] in ('<', '>'): # pagination across all nodes self._evaluate(elements) self._noderange = set(self.cfm.list_nodes()) else: self._noderange = self._evaluate(elements) @property def nodes(self): if self.beginpage is None and self.endpage is None: return self._noderange sortedlist = list(self._noderange) try: sortedlist.sort(key=humanify_nodename) except TypeError: # The natural sort attempt failed, fallback to ascii sort sortedlist.sort() if self.beginpage is not None: sortedlist = sortedlist[self.beginpage:] if self.endpage is not None: sortedlist = sortedlist[:self.endpage] return set(sortedlist) 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, filternodes) for elem in parsetree: if elem == ',-': current_op = 1 elif elem == ',': current_op = 0 elif elem == '@': current_op = 2 elif current_op == 0: current_range |= self._evaluate(elem) elif current_op == 1: current_range -= self._evaluate(elem, current_range) elif current_op == 2: current_range &= self._evaluate(elem, current_range) return current_range def failorreturn(self, atom): if self.cfm is not None: raise Exception(atom + " not valid") return set([atom]) def expandrange(self, seqrange, delimiter): pieces = seqrange.split(delimiter) if len(pieces) % 2 != 0: return self.failorreturn(seqrange) halflen = len(pieces) / 2 left = delimiter.join(pieces[:halflen]) right = delimiter.join(pieces[halflen:]) leftbits = _numextractor.parseString(left).asList() rightbits = _numextractor.parseString(right).asList() if len(leftbits) != len(rightbits): return self.failorreturn(seqrange) finalfmt = '' iterators = [] for idx in xrange(len(leftbits)): if leftbits[idx] == rightbits[idx]: finalfmt += leftbits[idx] elif leftbits[idx][0] in pp.alphas: # if string portion unequal, not going to work return self.failorreturn(seqrange) else: curseq = [] finalfmt += '{%d}' % len(iterators) iterators.append(curseq) leftnum = int(leftbits[idx]) rightnum = int(rightbits[idx]) if leftnum > rightnum: width = len(rightbits[idx]) minnum = rightnum maxnum = leftnum + 1 # xrange goes to n-1... elif rightnum > leftnum: width = len(leftbits[idx]) minnum = leftnum maxnum = rightnum + 1 else: # differently padded, but same number... return self.failorreturn(seqrange) numformat = '{0:0%d}' % width for num in xrange(minnum, maxnum): curseq.append(numformat.format(num)) results = set([]) for combo in itertools.product(*iterators): entname = finalfmt.format(*combo) results |= self.expand_entity(entname) return results def expand_entity(self, entname): if self.cfm is None or self.cfm.is_node(entname): return set([entname]) if self.cfm.is_nodegroup(entname): grpcfg = self.cfm.get_nodegroup_attributes(entname) nodes = grpcfg['nodes'] if 'noderange' in grpcfg and grpcfg['noderange']: nodes |= NodeRange( grpcfg['noderange']['value'], self.cfm).nodes return nodes raise Exception('Unknown node ' + entname) def _expandstring(self, element, filternodes=None): prefix = '' for idx in xrange(len(element)): if element[idx][0] == '[': nodes = set([]) for numeric in NodeRange(element[idx][1:-1]).nodes: nodes |= self._expandstring( [prefix + numeric] + element[idx + 1:]) return nodes else: prefix += element[idx] element = prefix if self.cfm is not None: # this is where we would check for exactly this if self.cfm.is_node(element): return set([element]) if self.cfm.is_nodegroup(element): grpcfg = self.cfm.get_nodegroup_attributes(element) nodes = grpcfg['nodes'] if 'noderange' in grpcfg and grpcfg['noderange']: nodes |= NodeRange( grpcfg['noderange']['value'], self.cfm).nodes return nodes if '-' in element and ':' not in element: return self.expandrange(element, '-') elif ':' in element: # : range for less ambiguity return self.expandrange(element, ':') 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 ('/', '~'): nameexpression = element[1:] if self.cfm is None: raise Exception('Verification configmanager required') return set(self.cfm.filter_nodenames(nameexpression, filternodes)) elif '+' in element: element, increment = element.split('+') try: nodename, domain = element.split('.') except ValueError: nodename = element domain = '' increment = int(increment) elembits = _numextractor.parseString(nodename).asList() endnum = str(int(elembits[-1]) + increment) left = ''.join(elembits) if domain: left += '.' + domain right = ''.join(elembits[:-1]) right += endnum if domain: right += '.' + domain nrange = left + ':' + right return self.expandrange(nrange, ':') elif '<' in element: self.beginpage = int(element[1:]) return set([]) elif '>' in element: self.endpage = int(element[1:]) return set([]) if self.cfm is None: return set([element]) raise Exception(element + ' not a recognized node, group, or alias')