Skip to content

Commit 636ceb7

Browse files
committed
NodeSet: fix % escaping when using _fromlist1 initializer
clush uses the private optimized NodeSet._fromlist1 constructor, and this one was lacking proper % character escaping. Cleaned up things so that escaping is done by main EngineParser methods, and not in low level _scan_* parsing methods anymore. Closes #275. Change-Id: Ib1f279a10ae82d655f0199cc753ed738a82e6ce5
1 parent cf0028e commit 636ceb7

File tree

3 files changed

+83
-24
lines changed

3 files changed

+83
-24
lines changed

lib/ClusterShell/NodeSet.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,14 @@ def __ixor__(self, other):
752752
return self
753753

754754

755+
def _strip_escape(nsstr):
756+
"""
757+
Helper to prepare a nodeset string for parsing: trim boundary
758+
whitespaces and escape special characters.
759+
"""
760+
return nsstr.strip().replace('%', '%%')
761+
762+
755763
class ParsingEngine(object):
756764
"""
757765
Class that is able to transform a source into a NodeSetBase.
@@ -801,6 +809,7 @@ def parse_string(self, nsstr, autostep, namespace=None):
801809
Return a NodeSetBase object.
802810
"""
803811
nodeset = NodeSetBase()
812+
nsstr = _strip_escape(nsstr)
804813

805814
for opc, pat, rgnd in self._scan_string(nsstr, autostep):
806815
# Parser main debugging:
@@ -825,8 +834,8 @@ def parse_string(self, nsstr, autostep, namespace=None):
825834

826835
def parse_string_single(self, nsstr, autostep):
827836
"""Parse provided string and return a NodeSetBase object."""
828-
# ignore node boundary whitespace(s)
829-
pat, rangesets = self._scan_string_single(nsstr.strip(), autostep)
837+
pat, rangesets = self._scan_string_single(_strip_escape(nsstr),
838+
autostep)
830839
if len(rangesets) > 1:
831840
rgobj = RangeSetND([rangesets], None, autostep, copy_rangeset=False)
832841
elif len(rangesets) == 1:
@@ -861,11 +870,14 @@ def parse_group_string(self, nodegroup, namespace=None):
861870
return ','.join(reslist), namespace
862871

863872
def grouplist(self, namespace=None):
864-
"""Return a sorted list of groups from current resolver (in optional
865-
group source / namespace)."""
873+
"""
874+
Return a sorted list of groups from current resolver (in optional
875+
group source / namespace).
876+
"""
866877
grpset = NodeSetBase()
867878
for grpstr in self.group_resolver.grouplist(namespace):
868879
# We scan each group string to expand any range seen...
880+
grpstr = _strip_escape(grpstr)
869881
for opc, pat, rgnd in self._scan_string(grpstr, None):
870882
getattr(grpset, opc)(NodeSetBase(pat, rgnd, False))
871883
return list(grpset)
@@ -947,20 +959,16 @@ def _scan_string_single(self, nsstr, autostep):
947959

948960
def _scan_string(self, nsstr, autostep):
949961
"""Parsing engine's string scanner method (iterator)."""
950-
pat = nsstr.strip()
951-
# avoid misformatting
952-
if pat.find('%') >= 0:
953-
pat = pat.replace('%', '%%')
954962
next_op_code = 'update'
955-
while pat is not None:
963+
while nsstr is not None:
956964
# Ignore whitespace(s) for convenience
957-
pat = pat.lstrip()
965+
nsstr = nsstr.lstrip()
958966

959967
rsets = []
960968
op_code = next_op_code
961969

962-
op_idx, next_op_code = self._next_op(pat)
963-
bracket_idx = pat.find(self.BRACKET_OPEN)
970+
op_idx, next_op_code = self._next_op(nsstr)
971+
bracket_idx = nsstr.find(self.BRACKET_OPEN)
964972

965973
# Check if the operator is after the bracket, or if there
966974
# is no operator at all but some brackets.
@@ -970,13 +978,13 @@ def _scan_string(self, nsstr, autostep):
970978
# Fill prefix, range and suffix from pattern
971979
# eg. "forbin[3,4-10]-ilo" -> "forbin", "3,4-10", "-ilo"
972980
newpat = ""
973-
sfx = pat
981+
sfx = nsstr
974982
while bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0):
975983
pfx, sfx = sfx.split(self.BRACKET_OPEN, 1)
976984
try:
977985
rng, sfx = sfx.split(self.BRACKET_CLOSE, 1)
978986
except ValueError:
979-
raise NodeSetParseError(pat, "missing bracket")
987+
raise NodeSetParseError(nsstr, "missing bracket")
980988

981989
# illegal closing bracket checks
982990
if pfx.find(self.BRACKET_CLOSE) > -1:
@@ -995,7 +1003,7 @@ def _scan_string(self, nsstr, autostep):
9951003

9961004
# pfx + sfx cannot be empty
9971005
if pfxlen + sfxlen == 0:
998-
raise NodeSetParseError(pat, "empty node name")
1006+
raise NodeSetParseError(nsstr, "empty node name")
9991007

10001008
if sfxlen > 0:
10011009
# amending trailing digits generates /steps
@@ -1031,12 +1039,12 @@ def _scan_string(self, nsstr, autostep):
10311039
# Check if we have a next op-separated node or pattern
10321040
op_idx, next_op_code = self._next_op(sfx)
10331041
if op_idx < 0:
1034-
pat = None
1042+
nsstr = None
10351043
else:
10361044
opc = self.OP_CODES[next_op_code]
1037-
sfx, pat = sfx.split(opc, 1)
1045+
sfx, nsstr = sfx.split(opc, 1)
10381046
# Detected character operator so right operand is mandatory
1039-
if not pat:
1047+
if not nsstr:
10401048
msg = "missing nodeset operand with '%s' operator" % opc
10411049
raise NodeSetParseError(None, msg)
10421050

@@ -1049,22 +1057,22 @@ def _scan_string(self, nsstr, autostep):
10491057

10501058
# pfx + sfx cannot be empty
10511059
if len(newpat) == 0:
1052-
raise NodeSetParseError(pat, "empty node name")
1060+
raise NodeSetParseError(nsstr, "empty node name")
10531061

10541062
else:
10551063
# In this case, either there is no comma and no bracket,
10561064
# or the bracket is after the comma, then just return
10571065
# the node.
10581066
if op_idx < 0:
1059-
node = pat
1060-
pat = None # break next time
1067+
node = nsstr
1068+
nsstr = None # break next time
10611069
else:
10621070
opc = self.OP_CODES[next_op_code]
1063-
node, pat = pat.split(opc, 1)
1071+
node, nsstr = nsstr.split(opc, 1)
10641072
# Detected character operator so both operands are mandatory
1065-
if not node or not pat:
1073+
if not node or not nsstr:
10661074
msg = "missing nodeset operand with '%s' operator" % opc
1067-
raise NodeSetParseError(node or pat, msg)
1075+
raise NodeSetParseError(node or nsstr, msg)
10681076

10691077
# Check for illegal closing bracket
10701078
if node.find(self.BRACKET_CLOSE) > -1:

tests/NodeSetGroupTest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def makeTestG2():
5757
#
5858
para: montana[32-37,42-55]
5959
gpu: montana[38-41]
60+
escape%test: montana[87-90]
61+
esc%test2: @escape%test
6062
""")
6163
return f2
6264

@@ -960,6 +962,36 @@ def testConfigCFGDIR(self):
960962
self.assertEqual(str(NodeSet("@%s" % tmpgroup, resolver=res)),
961963
"example[1-100]")
962964

965+
def test_fromall_grouplist(self):
966+
"""test NodeSet.fromall() without all upcall"""
967+
# Group Source that has no all upcall and that can handle special char
968+
test_groups2 = makeTestG2()
969+
970+
source = UpcallGroupSource("simple",
971+
"sed -n 's/^$GROUP:\(.*\)/\\1/p' %s" % test_groups2.name,
972+
None,
973+
"sed -n 's/^\([0-9A-Za-z_-\%%]*\):.*/\\1/p' %s"
974+
% test_groups2.name,
975+
None)
976+
res = GroupResolver(source)
977+
978+
# fromall will trigger ParserEngine.grouplist() that we want to test here
979+
nsall = NodeSet.fromall(resolver=res)
980+
981+
# if working, group resolution worked with % char
982+
self.assertEqual(str(NodeSet.fromall(resolver=res)), "montana[32-55,87-90]")
983+
self.assertEqual(len(nsall), 28)
984+
985+
# btw explicitly check escaped char
986+
nsesc = NodeSet('@escape%test', resolver=res)
987+
self.assertEqual(str(nsesc), 'montana[87-90]')
988+
self.assertEqual(len(nsesc), 4)
989+
nsesc2 = NodeSet('@esc%test2', resolver=res)
990+
self.assertEqual(nsesc, nsesc2)
991+
ns = NodeSet('montana[87-90]', resolver=res)
992+
# could also result in escape%test?
993+
self.assertEqual(ns.regroup(), '@esc%test2')
994+
963995

964996
class NodeSetGroup2GSTest(unittest.TestCase):
965997

tests/NodeSetTest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ def testNodeWithPercent(self):
100100
self.assertEqual(str(nodeset), "clust%ser[3-30]")
101101
nodeset = NodeSet("myclu%ster,clust%ser[3-30]")
102102
self.assertEqual(str(nodeset), "clust%ser[3-30],myclu%ster")
103+
# issue #275
104+
nodeset = NodeSet.fromlist(["cluster%eth0", "cluster%eth1"])
105+
self.assertEqual(str(nodeset), "cluster%eth[0-1]")
106+
nodeset = NodeSet.fromlist(["cluster%eth[0-8]", "cluster%eth9"])
107+
self.assertEqual(str(nodeset), "cluster%eth[0-9]")
108+
nodeset = NodeSet.fromlist(["super%cluster", "hyper%cluster"])
109+
self.assertEqual(str(nodeset), "hyper%cluster,super%cluster")
110+
# test also private _fromlist1 constructor
111+
nodeset = NodeSet._fromlist1(["cluster%eth0", "cluster%eth1"])
112+
self.assertEqual(str(nodeset), "cluster%eth[0-1]")
113+
nodeset = NodeSet._fromlist1(["super%cluster", "hyper%cluster"])
114+
self.assertEqual(str(nodeset), "hyper%cluster,super%cluster")
115+
# real use-case!? exercise nD and escaping!
116+
nodeset = NodeSet("fe80::5054:ff:feff:6944%eth0 ")
117+
self._assertNode(nodeset, "fe80::5054:ff:feff:6944%eth0")
118+
nodeset = NodeSet.fromlist(["fe80::5054:ff:feff:6944%eth0"])
119+
self._assertNode(nodeset, "fe80::5054:ff:feff:6944%eth0")
120+
nodeset = NodeSet._fromlist1(["fe80::5054:ff:feff:6944%eth0"])
121+
self._assertNode(nodeset, "fe80::5054:ff:feff:6944%eth0")
103122

104123
def testNodeEightPad(self):
105124
"""test NodeSet padding feature"""

0 commit comments

Comments
 (0)