# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2014 IBM Corporation # # 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)) _grammar = _nodeatom | ',-' | ',' | '@' _parser = pp.nestedExpr(content=_grammar) _numextractor = pp.OneOrMore(pp.Word(pp.alphas + '-') | pp.Word(pp.nums)) #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 verify: whether or not to perform lookups in the config """ def __init__(self, noderange, verify=True): self.verify = verify elements = _parser.parseString("(" + noderange + ")").asList()[0] self._noderange = self._evaluate(elements) @property def nodes(self): return self._noderange def _evaluate(self, parsetree): 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) 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) elif current_op == 2: current_range &= self._evaluate(elem) return current_range def failorreturn(self, atom): if self.verify: raise Exception(atom + " not valid") return set([atom]) def expandrange(self, range, delimiter): pieces = range.split(delimiter) if len(pieces) % 2 != 0: return self.failorreturn(range) 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(range) 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(range) 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(range) numformat = '{0:0%d}' % width for num in xrange(minnum, maxnum): curseq.append(numformat.format(num)) results = set([]) for combo in itertools.product(*iterators): results.add(finalfmt.format(*combo)) return results def _expandstring(self, element): prefix = '' for idx in xrange(len(element)): if element[idx][0] == '[': nodes = set([]) for numeric in NodeRange(element[idx][1:-1], False).nodes: nodes |= self._expandstring( [prefix + numeric] + element[idx + 1:]) return nodes else: prefix += element[idx] element = prefix nodes = set([]) if self.verify: #this is where we would check for exactly this raise Exception("TODO: link with actual config") #this is where we would check for a literal groupname #ok, now time to understand the various things 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: raise Exception('TODO: criteria noderange') elif element[0] in ('/', '~'): raise Exception('TODO: regex noderange') elif '+' in element: raise Exception('TODO: plus range') if not self.verify: return set([element])