2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-02-05 21:42:24 +00:00
Jarrod Johnson 58157b23d7 Error if noderange includes too many )
By default, pyparsing consumes only as much of the
input as matches the grammar. Tell it to consume all
of the noderange and error if there's more string than
matches our grammar.
2021-06-04 14:19:06 -04:00

285 lines
11 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2017 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 copy
import itertools
import pyparsing as pp
import re
try:
range = xrange
except NameError:
pass
# 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]+)')
lastnoderange = None
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)]
class ReverseNodeRange(object):
"""Abbreviate a set of nodes to a shorter noderange representation
:param nodes: List of nodes as a list, tuple, etc.
:param config: Config manager
"""
def __init__(self, nodes, config):
self.cfm = config
self.nodes = set(nodes)
@property
def noderange(self):
subsetgroups = []
for group in self.cfm.get_groups(sizesort=True):
if lastnoderange:
for nr in lastnoderange:
if lastnoderange[nr] - self.nodes:
continue
if self.nodes - lastnoderange[nr]:
continue
return nr
nl = set(
self.cfm.get_nodegroup_attributes(group).get('nodes', []))
if len(nl) > len(self.nodes) or not nl:
continue
if not nl - self.nodes:
subsetgroups.append(group)
self.nodes -= nl
if not self.nodes:
break
return ','.join(sorted(subsetgroups) + sorted(self.nodes))
# 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):
global lastnoderange
self.beginpage = None
self.endpage = None
self.cfm = config
try:
elements = _parser.parseString("(" + noderange + ")", parseAll=True).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)
lastnoderange = {noderange: set(self._noderange)}
@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 range(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 # range 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 range(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 = copy.copy(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 = ''
if element[0][0] in ('/', '~'):
element = ''.join(element)
nameexpression = element[1:]
if self.cfm is None:
raise Exception('Verification configmanager required')
return set(self.cfm.filter_nodenames(nameexpression, filternodes))
elif '=' in element[0] or '!~' in element[0]:
element = ''.join(element)
if self.cfm is None:
raise Exception('Verification configmanager required')
return set(self.cfm.filter_node_attributes(element, filternodes))
for idx in range(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 = copy.copy(grpcfg['nodes'])
if 'noderange' in grpcfg and grpcfg['noderange']:
nodes |= NodeRange(
grpcfg['noderange']['value'], self.cfm).nodes
return nodes
if ':' in element: # : range for less ambiguity
return self.expandrange(element, ':')
elif '..' in element:
return self.expandrange(element, '..')
elif '-' in element:
return self.expandrange(element, '-')
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')