2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-01-18 05:33:17 +00:00
Jarrod Johnson 1c6430bf3f Allow noderange pagination of all nodes
When a noderange starts with '<' or '>', use the set of all nodes as basis for pagination.
Additionally, provide better feedback to client on noderange parsing issues.  Also
implement natural sort in various places after doing it for the pagination.
2015-03-25 09:57:25 -04:00

234 lines
9.2 KiB
Python

# 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 and >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')