1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 Cluster node set module.
24
25 A module to efficiently deal with node sets and node groups.
26 Instances of NodeSet provide similar operations than the builtin set() type,
27 see http://www.python.org/doc/lib/set-objects.html
28
29 Usage example
30 =============
31 >>> # Import NodeSet class
32 ... from ClusterShell.NodeSet import NodeSet
33 >>>
34 >>> # Create a new nodeset from string
35 ... nodeset = NodeSet("cluster[1-30]")
36 >>> # Add cluster32 to nodeset
37 ... nodeset.update("cluster32")
38 >>> # Remove from nodeset
39 ... nodeset.difference_update("cluster[2-5,8-31]")
40 >>> # Print nodeset as a pdsh-like pattern
41 ... print nodeset
42 cluster[1,6-7,32]
43 >>> # Iterate over node names in nodeset
44 ... for node in nodeset:
45 ... print node
46 cluster1
47 cluster6
48 cluster7
49 cluster32
50 """
51
52 import re
53 import string
54 import sys
55
56 from ClusterShell.Defaults import config_paths
57 import ClusterShell.NodeUtils as NodeUtils
58
59
60 from ClusterShell.RangeSet import RangeSet, RangeSetND, AUTOSTEP_DISABLED
61 from ClusterShell.RangeSet import RangeSetException, RangeSetParseError
62 from ClusterShell.RangeSet import RangeSetPaddingError
63
64
65
66 DEF_GROUPS_CONFIGS = config_paths('groups.conf')
67 ILLEGAL_GROUP_CHARS = set("@,!&^*")
68 _DEF_RESOLVER_STD_GROUP = NodeUtils.GroupResolverConfig(DEF_GROUPS_CONFIGS,
69 ILLEGAL_GROUP_CHARS)
70
71 RESOLVER_STD_GROUP = _DEF_RESOLVER_STD_GROUP
72
73
74
75 RESOLVER_NOGROUP = -1
76 RESOLVER_NOINIT = -2
77
78 STD_GROUP_RESOLVER = RESOLVER_STD_GROUP
79 NOGROUP_RESOLVER = RESOLVER_NOGROUP
83 """Base NodeSet exception class."""
84
86 """Raised when an error is encountered."""
87
89 """Raised when NodeSet parsing cannot be done properly."""
96
98 """Raised when bad range is encountered during NodeSet parsing."""
101
102 -class NodeSetExternalError(NodeSetError):
103 """Raised when an external error is encountered."""
104
107 """
108 Base class for NodeSet.
109
110 This class allows node set base object creation from specified string
111 pattern and rangeset object. If optional copy_rangeset boolean flag is
112 set to True (default), provided rangeset object is copied (if needed),
113 otherwise it may be referenced (should be seen as an ownership transfer
114 upon creation).
115
116 This class implements core node set arithmetics (no string parsing here).
117
118 Example:
119 >>> nsb = NodeSetBase('node%s-ipmi', RangeSet('1-5,7'), False)
120 >>> str(nsb)
121 'node[1-5,7]-ipmi'
122 >>> nsb = NodeSetBase('node%s-ib%s', RangeSetND([['1-5,7', '1-2']]), False)
123 >>> str(nsb)
124 'node[1-5,7]-ib[1-2]'
125 """
126 - def __init__(self, pattern=None, rangeset=None, copy_rangeset=True,
127 autostep=None, fold_axis=None):
128 """New NodeSetBase object initializer"""
129 self._autostep = autostep
130 self._length = 0
131 self._patterns = {}
132 self.fold_axis = fold_axis
133 if pattern:
134 self._add(pattern, rangeset, copy_rangeset)
135 elif rangeset:
136 raise ValueError("missing pattern")
137
139 """Get autostep value (property)"""
140 return self._autostep
141
143 """Set autostep value (property)"""
144 if val is None:
145 self._autostep = None
146 else:
147
148 self._autostep = min(int(val), AUTOSTEP_DISABLED)
149
150
151 for pat, rset in self._patterns.iteritems():
152 if rset:
153 rset.autostep = self._autostep
154
155 autostep = property(get_autostep, set_autostep)
156
158 """Iterator on internal item tuples
159 (pattern, indexes, padding, autostep)."""
160 for pat, rset in sorted(self._patterns.iteritems()):
161 if rset:
162 autostep = rset.autostep
163 if rset.dim() == 1:
164 assert isinstance(rset, RangeSet)
165 padding = rset.padding
166 for idx in rset:
167 yield pat, (idx,), (padding,), autostep
168 else:
169 for args, padding in rset.iter_padding():
170 yield pat, args, padding, autostep
171 else:
172 yield pat, None, None, None
173
175 """Iterator on single, one-item NodeSetBase objects."""
176 for pat, ivec, pad, autostep in self._iter():
177 rset = None
178 if ivec is not None:
179 assert len(ivec) > 0
180 if len(ivec) == 1:
181 rset = RangeSet.fromone(ivec[0], pad[0] or 0, autostep)
182 else:
183 rset = RangeSetND([ivec], pad, autostep)
184 yield NodeSetBase(pat, rset)
185
187 """Iterator on single nodes as string."""
188
189 for pat, ivec, pads, _ in self._iter():
190 if ivec is not None:
191
192 if len(ivec) == 1:
193 yield pat % ("%0*d" % (pads[0] or 0, ivec[0]))
194 else:
195 yield pat % tuple(["%0*d" % (pad or 0, i) \
196 for pad, i in zip(pads, ivec)])
197 else:
198 yield pat % ()
199
200
201 striter = __iter__
202
203
204
205
219
221 """Object-based NodeSet iterator on contiguous node sets.
222
223 Contiguous node set contains nodes with same pattern name and a
224 contiguous range of indexes, like foobar[1-100]."""
225 for pat, rangeset in sorted(self._patterns.iteritems()):
226 if rangeset:
227 for cont_rset in rangeset.contiguous():
228 nodeset = self.__class__()
229 nodeset._add_new(pat, cont_rset)
230 yield nodeset
231 else:
232 nodeset = self.__class__()
233 nodeset._add_new(pat, None)
234 yield nodeset
235
237 """Get the number of nodes in NodeSet."""
238 cnt = 0
239 for rangeset in self._patterns.itervalues():
240 if rangeset:
241 cnt += len(rangeset)
242 else:
243 cnt += 1
244 return cnt
245
247 """
248 Take a pattern and a RangeSetND object and iterate over nD computed
249 nodeset strings while following fold_axis constraints.
250 """
251 try:
252 dimcnt = rset.dim()
253 if self.fold_axis is None:
254
255 fold_axis = range(dimcnt)
256 else:
257
258 fold_axis = [int(x) % dimcnt for x in self.fold_axis
259 if -dimcnt <= int(x) < dimcnt]
260 except (TypeError, ValueError), exc:
261 raise NodeSetParseError("fold_axis=%s" % self.fold_axis, exc)
262
263 for rgvec in rset.vectors():
264 rgnargs = []
265 for axis, rangeset in enumerate(rgvec):
266
267 if len(rangeset) > 1:
268 if axis not in fold_axis:
269 rgstrit = rangeset.striter()
270 else:
271 rgstrit = ["[%s]" % rangeset]
272 else:
273 rgstrit = [str(rangeset)]
274
275
276 t_rgnargs = []
277 for rgstr in rgstrit:
278 if not rgnargs:
279 t_rgnargs.append([rgstr])
280 else:
281 for rga in rgnargs:
282 t_rgnargs.append(rga + [rgstr])
283 rgnargs = t_rgnargs
284
285
286 for rgargs in rgnargs:
287 yield pat % tuple(rgargs)
288
290 """Get ranges-based pattern of node list."""
291 results = []
292 try:
293 for pat, rset in sorted(self._patterns.iteritems()):
294 if not rset:
295 results.append(pat % ())
296 elif rset.dim() == 1:
297
298 if self.fold_axis is None or \
299 list(x for x in self.fold_axis if -1 <= int(x) < 1):
300 rgs = str(rset)
301 cnt = len(rset)
302 if cnt > 1:
303 rgs = "[%s]" % rgs
304 results.append(pat % rgs)
305 else:
306 results.extend((pat % rgs for rgs in rset.striter()))
307 elif rset.dim() > 1:
308 results.extend(self._iter_nd_pat(pat, rset))
309 except TypeError:
310 raise NodeSetParseError(pat, "Internal error: node pattern and "
311 "ranges mismatch")
312 return ",".join(results)
313
315 """Return a shallow copy."""
316 cpy = self.__class__()
317 cpy.fold_axis = self.fold_axis
318 cpy._autostep = self._autostep
319 cpy._length = self._length
320 dic = {}
321 for pat, rangeset in self._patterns.iteritems():
322 if rangeset is None:
323 dic[pat] = None
324 else:
325 dic[pat] = rangeset.copy()
326 cpy._patterns = dic
327 return cpy
328
330 """Is node contained in NodeSet ?"""
331 return self.issuperset(other)
332
334
335
336 if not isinstance(other, NodeSetBase):
337 raise TypeError, \
338 "Binary operation only permitted between NodeSetBase"
339
344
346 """Report whether this nodeset contains another nodeset."""
347 self._binary_sanity_check(other)
348 status = True
349 for pat, erangeset in other._patterns.iteritems():
350 rangeset = self._patterns.get(pat)
351 if rangeset:
352 status = rangeset.issuperset(erangeset)
353 else:
354
355 status = self._patterns.has_key(pat)
356 if not status:
357 break
358 return status
359
361 """NodeSet equality comparison."""
362
363 if not isinstance(other, NodeSetBase):
364 return NotImplemented
365 return len(self) == len(other) and self.issuperset(other)
366
367
368 __le__ = issubset
369 __ge__ = issuperset
370
375
380
382 """Private utility function: extract slice parameters from slice object
383 `index` for an list-like object of size `length`."""
384 length = len(self)
385 if index.start is None:
386 sl_start = 0
387 elif index.start < 0:
388 sl_start = max(0, length + index.start)
389 else:
390 sl_start = index.start
391 if index.stop is None:
392 sl_stop = sys.maxint
393 elif index.stop < 0:
394 sl_stop = max(0, length + index.stop)
395 else:
396 sl_stop = index.stop
397 if index.step is None:
398 sl_step = 1
399 elif index.step < 0:
400
401 if index.start is not None or index.stop is not None:
402 raise IndexError, \
403 "illegal start and stop when negative step is used"
404
405
406 stepmod = (length + -index.step - 1) % -index.step
407 if stepmod > 0:
408 sl_start += stepmod
409 sl_step = -index.step
410 else:
411 sl_step = index.step
412 if not isinstance(sl_start, int) or not isinstance(sl_stop, int) \
413 or not isinstance(sl_step, int):
414 raise TypeError, "slice indices must be integers"
415 return sl_start, sl_stop, sl_step
416
418 """Return the node at specified index or a subnodeset when a slice is
419 specified."""
420 if isinstance(index, slice):
421 inst = NodeSetBase()
422 sl_start, sl_stop, sl_step = self._extractslice(index)
423 sl_next = sl_start
424 if sl_stop <= sl_next:
425 return inst
426 length = 0
427 for pat, rangeset in sorted(self._patterns.iteritems()):
428 if rangeset:
429 cnt = len(rangeset)
430 offset = sl_next - length
431 if offset < cnt:
432 num = min(sl_stop - sl_next, cnt - offset)
433 inst._add(pat, rangeset[offset:offset + num:sl_step])
434 else:
435
436 length += cnt
437 continue
438 else:
439 cnt = num = 1
440 if sl_next > length:
441 length += cnt
442 continue
443 inst._add(pat, None)
444
445 sl_next += num
446 if (sl_next - sl_start) % sl_step:
447 sl_next = sl_start + \
448 ((sl_next - sl_start)/sl_step + 1) * sl_step
449 if sl_next >= sl_stop:
450 break
451 length += cnt
452 return inst
453 elif isinstance(index, int):
454 if index < 0:
455 length = len(self)
456 if index >= -length:
457 index = length + index
458 else:
459 raise IndexError, "%d out of range" % index
460 length = 0
461 for pat, rangeset in sorted(self._patterns.iteritems()):
462 if rangeset:
463 cnt = len(rangeset)
464 if index < length + cnt:
465
466 if rangeset.dim() == 1:
467 return pat % rangeset[index-length:index-length+1]
468 else:
469 sub = rangeset[index-length:index-length+1]
470 for rgvec in sub.vectors():
471 return pat % (tuple(rgvec))
472 else:
473 cnt = 1
474 if index == length:
475 return pat
476 length += cnt
477 raise IndexError, "%d out of range" % index
478 else:
479 raise TypeError, "NodeSet indices must be integers"
480
482 """Add nodes from a (pat, rangeset) tuple.
483 Predicate: pattern does not exist in current set. RangeSet object is
484 referenced (not copied)."""
485 assert pat not in self._patterns
486 self._patterns[pat] = rangeset
487
488 - def _add(self, pat, rangeset, copy_rangeset=True):
489 """Add nodes from a (pat, rangeset) tuple.
490 `pat' may be an existing pattern and `rangeset' may be None.
491 RangeSet or RangeSetND objects are copied if re-used internally
492 when provided and if copy_rangeset flag is set.
493 """
494 if pat in self._patterns:
495
496 pat_e = self._patterns[pat]
497
498 if (pat_e is None) is not (rangeset is None):
499 raise NodeSetError("Invalid operation")
500
501 if pat_e:
502 pat_e.update(rangeset)
503 else:
504
505 if rangeset and copy_rangeset:
506
507 rangeset = rangeset.copy()
508
509 if self._autostep is not None:
510
511 rangeset.autostep = self._autostep
512 self._add_new(pat, rangeset)
513
515 """
516 s.union(t) returns a new set with elements from both s and t.
517 """
518 self_copy = self.copy()
519 self_copy.update(other)
520 return self_copy
521
523 """
524 Implements the | operator. So s | t returns a new nodeset with
525 elements from both s and t.
526 """
527 if not isinstance(other, NodeSetBase):
528 return NotImplemented
529 return self.union(other)
530
531 - def add(self, other):
532 """
533 Add node to NodeSet.
534 """
535 self.update(other)
536
538 """
539 s.update(t) returns nodeset s with elements added from t.
540 """
541 for pat, rangeset in other._patterns.iteritems():
542 self._add(pat, rangeset)
543
545 """
546 s.updaten(list) returns nodeset s with elements added from given list.
547 """
548 for other in others:
549 self.update(other)
550
552 """
553 Remove all nodes from this nodeset.
554 """
555 self._patterns.clear()
556
558 """
559 Implements the |= operator. So ``s |= t`` returns nodeset s with
560 elements added from t. (Python version 2.5+ required)
561 """
562 self._binary_sanity_check(other)
563 self.update(other)
564 return self
565
567 """
568 s.intersection(t) returns a new set with elements common to s
569 and t.
570 """
571 self_copy = self.copy()
572 self_copy.intersection_update(other)
573 return self_copy
574
576 """
577 Implements the & operator. So ``s & t`` returns a new nodeset with
578 elements common to s and t.
579 """
580 if not isinstance(other, NodeSet):
581 return NotImplemented
582 return self.intersection(other)
583
585 """
586 ``s.intersection_update(t)`` returns nodeset s keeping only
587 elements also found in t.
588 """
589 if other is self:
590 return
591
592 tmp_ns = NodeSetBase()
593
594 for pat, irangeset in other._patterns.iteritems():
595 rangeset = self._patterns.get(pat)
596 if rangeset:
597 irset = rangeset.intersection(irangeset)
598
599 if len(irset) > 0:
600 tmp_ns._add(pat, irset, copy_rangeset=False)
601 elif not irangeset and pat in self._patterns:
602
603 tmp_ns._add(pat, None)
604
605
606 self._patterns = tmp_ns._patterns
607
609 """
610 Implements the &= operator. So ``s &= t`` returns nodeset s keeping
611 only elements also found in t. (Python version 2.5+ required)
612 """
613 self._binary_sanity_check(other)
614 self.intersection_update(other)
615 return self
616
618 """
619 ``s.difference(t)`` returns a new NodeSet with elements in s but not
620 in t.
621 """
622 self_copy = self.copy()
623 self_copy.difference_update(other)
624 return self_copy
625
627 """
628 Implement the - operator. So ``s - t`` returns a new nodeset with
629 elements in s but not in t.
630 """
631 if not isinstance(other, NodeSetBase):
632 return NotImplemented
633 return self.difference(other)
634
636 """
637 ``s.difference_update(t)`` removes from s all the elements found in t.
638
639 :raises KeyError: an element cannot be removed (only if strict is
640 True)
641 """
642
643 purge_patterns = []
644
645
646 for pat, erangeset in other._patterns.iteritems():
647
648 rangeset = self._patterns.get(pat)
649 if rangeset:
650
651 rangeset.difference_update(erangeset, strict)
652
653
654 if len(rangeset) == 0:
655 purge_patterns.append(pat)
656 else:
657
658 if self._patterns.has_key(pat):
659 purge_patterns.append(pat)
660 elif strict:
661 raise KeyError, pat
662
663 for pat in purge_patterns:
664 del self._patterns[pat]
665
667 """
668 Implement the -= operator. So ``s -= t`` returns nodeset s after
669 removing elements found in t. (Python version 2.5+ required)
670 """
671 self._binary_sanity_check(other)
672 self.difference_update(other)
673 return self
674
676 """
677 Remove element elem from the nodeset. Raise KeyError if elem
678 is not contained in the nodeset.
679
680 :raises KeyError: elem is not contained in the nodeset
681 """
682 self.difference_update(elem, True)
683
685 """
686 ``s.symmetric_difference(t)`` returns the symmetric difference of
687 two nodesets as a new NodeSet.
688
689 (ie. all nodes that are in exactly one of the nodesets.)
690 """
691 self_copy = self.copy()
692 self_copy.symmetric_difference_update(other)
693 return self_copy
694
696 """
697 Implement the ^ operator. So ``s ^ t`` returns a new NodeSet with
698 nodes that are in exactly one of the nodesets.
699 """
700 if not isinstance(other, NodeSet):
701 return NotImplemented
702 return self.symmetric_difference(other)
703
705 """
706 ``s.symmetric_difference_update(t)`` returns nodeset s keeping all
707 nodes that are in exactly one of the nodesets.
708 """
709 purge_patterns = []
710
711
712 for pat, rangeset in self._patterns.iteritems():
713 brangeset = other._patterns.get(pat)
714 if brangeset:
715 rangeset.symmetric_difference_update(brangeset)
716 else:
717 if other._patterns.has_key(pat):
718 purge_patterns.append(pat)
719
720
721 for pat, brangeset in other._patterns.iteritems():
722 rangeset = self._patterns.get(pat)
723 if not rangeset and not pat in self._patterns:
724 self._add(pat, brangeset)
725
726
727 for pat, rangeset in self._patterns.iteritems():
728 if rangeset is not None and len(rangeset) == 0:
729 purge_patterns.append(pat)
730
731
732 for pat in purge_patterns:
733 del self._patterns[pat]
734
736 """
737 Implement the ^= operator. So ``s ^= t`` returns nodeset s after
738 keeping all nodes that are in exactly one of the nodesets.
739 (Python version 2.5+ required)
740 """
741 self._binary_sanity_check(other)
742 self.symmetric_difference_update(other)
743 return self
744
747 """
748 Helper to prepare a nodeset string for parsing: trim boundary
749 whitespaces and escape special characters.
750 """
751 return nsstr.strip().replace('%', '%%')
752
755 """
756 Class that is able to transform a source into a NodeSetBase.
757 """
758 OP_CODES = { 'update': ',',
759 'difference_update': '!',
760 'intersection_update': '&',
761 'symmetric_difference_update': '^' }
762
763 BRACKET_OPEN = '['
764 BRACKET_CLOSE = ']'
765
767 """
768 Initialize Parsing Engine.
769 """
770 self.group_resolver = group_resolver
771 self.base_node_re = re.compile("(\D*)(\d*)")
772
773 - def parse(self, nsobj, autostep):
774 """
775 Parse provided object if possible and return a NodeSetBase object.
776 """
777
778 if nsobj is None:
779 return NodeSetBase()
780
781
782 if isinstance(nsobj, NodeSetBase):
783 return nsobj
784
785
786 if isinstance(nsobj, basestring):
787 try:
788 return self.parse_string(str(nsobj), autostep)
789 except (NodeUtils.GroupSourceQueryFailed, RuntimeError), exc:
790 raise NodeSetParseError(nsobj, str(exc))
791
792 raise TypeError("Unsupported NodeSet input %s" % type(nsobj))
793
795 """Parse provided string in optional namespace.
796
797 This method parses string, resolves all node groups, and
798 computes set operations.
799
800 Return a NodeSetBase object.
801 """
802 nodeset = NodeSetBase()
803 nsstr = _strip_escape(nsstr)
804
805 for opc, pat, rgnd in self._scan_string(nsstr, autostep):
806
807
808 if self.group_resolver and pat[0] == '@':
809 ns_group = NodeSetBase()
810 for nodegroup in NodeSetBase(pat, rgnd):
811
812 ns_str_ext, ns_nsp_ext = self.parse_group_string(nodegroup,
813 namespace)
814 if ns_str_ext:
815
816 ns_group.update(self.parse_string(ns_str_ext,
817 autostep,
818 ns_nsp_ext))
819
820 getattr(nodeset, opc)(ns_group)
821 else:
822 getattr(nodeset, opc)(NodeSetBase(pat, rgnd, False))
823
824 return nodeset
825
827 """Parse provided string and return a NodeSetBase object."""
828 pat, rangesets = self._scan_string_single(_strip_escape(nsstr),
829 autostep)
830 if len(rangesets) > 1:
831 rgobj = RangeSetND([rangesets], None, autostep, copy_rangeset=False)
832 elif len(rangesets) == 1:
833 rgobj = rangesets[0]
834 else:
835 rgobj = None
836 return NodeSetBase(pat, rgobj, False)
837
838 - def parse_group(self, group, namespace=None, autostep=None):
839 """Parse provided single group name (without @ prefix)."""
840 assert self.group_resolver is not None
841 nodestr = self.group_resolver.group_nodes(group, namespace)
842 return self.parse(",".join(nodestr), autostep)
843
845 """Parse provided raw nodegroup string in optional namespace.
846
847 Warning: 1 pass only, may still return groups.
848
849 Return a tuple (grp_resolved_string, namespace).
850 """
851 assert nodegroup[0] == '@'
852 assert self.group_resolver is not None
853 grpstr = group = nodegroup[1:]
854 if grpstr.find(':') >= 0:
855
856 namespace, group = grpstr.split(':', 1)
857 if group == '*':
858 reslist = self.all_nodes(namespace)
859 else:
860 reslist = self.group_resolver.group_nodes(group, namespace)
861 return ','.join(reslist), namespace
862
864 """
865 Return a sorted list of groups from current resolver (in optional
866 group source / namespace).
867 """
868 grpset = NodeSetBase()
869 for grpstr in self.group_resolver.grouplist(namespace):
870
871 grpstr = _strip_escape(grpstr)
872 for opc, pat, rgnd in self._scan_string(grpstr, None):
873 getattr(grpset, opc)(NodeSetBase(pat, rgnd, False))
874 return list(grpset)
875
898
900 """Opcode parsing subroutine."""
901 op_idx = -1
902 next_op_code = None
903 for opc, idx in [(k, pat.find(v)) \
904 for k, v in ParsingEngine.OP_CODES.iteritems()]:
905 if idx >= 0 and (op_idx < 0 or idx <= op_idx):
906 next_op_code = opc
907 op_idx = idx
908 return op_idx, next_op_code
909
911 """Single node scan, returns (pat, list of rangesets)"""
912
913 pfx_nd = [mobj.groups() for mobj in self.base_node_re.finditer(nsstr)]
914 pfx_nd = pfx_nd[:-1]
915 if not pfx_nd:
916 raise NodeSetParseError(nsstr, "parse error")
917
918
919 if len(pfx_nd) == 1 and len(pfx_nd[0][0]) == 0:
920 raise NodeSetParseError(nsstr, "empty node name")
921
922 pat = ""
923 rangesets = []
924 for pfx, idx in pfx_nd:
925 if idx:
926
927 pad = 0
928 if int(idx) != 0:
929 idxs = idx.lstrip("0")
930 if len(idx) - len(idxs) > 0:
931 pad = len(idx)
932 idxint = int(idxs)
933 else:
934 if len(idx) > 1:
935 pad = len(idx)
936 idxint = 0
937 if idxint > 1e100:
938 raise NodeSetParseRangeError( \
939 RangeSetParseError(idx, "invalid rangeset index"))
940
941 pat += "%s%%s" % pfx
942 rangesets.append(RangeSet.fromone(idxint, pad, autostep))
943 else:
944
945 pat += pfx
946 return pat, rangesets
947
949 """Parsing engine's string scanner method (iterator)."""
950 next_op_code = 'update'
951 while nsstr:
952
953 nsstr = nsstr.lstrip()
954
955 rsets = []
956 op_code = next_op_code
957
958 op_idx, next_op_code = self._next_op(nsstr)
959 bracket_idx = nsstr.find(self.BRACKET_OPEN)
960
961
962
963 if bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0):
964
965
966
967
968 newpat = ""
969 sfx = nsstr
970 while bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0):
971 pfx, sfx = sfx.split(self.BRACKET_OPEN, 1)
972 try:
973 rng, sfx = sfx.split(self.BRACKET_CLOSE, 1)
974 except ValueError:
975 raise NodeSetParseError(nsstr, "missing bracket")
976
977
978 if pfx.find(self.BRACKET_CLOSE) > -1:
979 raise NodeSetParseError(pfx, "illegal closing bracket")
980
981 if len(sfx) > 0:
982 bra_end = sfx.find(self.BRACKET_CLOSE)
983 bra_start = sfx.find(self.BRACKET_OPEN)
984 if bra_start == -1:
985 bra_start = bra_end + 1
986 if bra_end >= 0 and bra_end < bra_start:
987 msg = "illegal closing bracket"
988 raise NodeSetParseError(sfx, msg)
989
990 pfxlen, sfxlen = len(pfx), len(sfx)
991
992 if sfxlen > 0:
993
994 sfx, rng = self._amend_trailing_digits(sfx, rng)
995
996 if pfxlen > 0:
997
998 pfx, rng = self._amend_leading_digits(pfx, rng)
999 if pfx:
1000
1001 pfx, pfxrvec = self._scan_string_single(pfx, autostep)
1002 rsets += pfxrvec
1003
1004
1005 bracket_idx = sfx.find(self.BRACKET_OPEN,
1006 bracket_idx - pfxlen)
1007 op_idx, next_op_code = self._next_op(sfx)
1008
1009
1010 if len(pfx) == 0 and op_idx == 0:
1011 raise NodeSetParseError(sfx, "empty node name before")
1012
1013 if len(sfx) > 0 and sfx[0] == '[':
1014 msg = "illegal reopening bracket"
1015 raise NodeSetParseError(sfx, msg)
1016
1017 newpat += "%s%%s" % pfx
1018 try:
1019 rsets.append(RangeSet(rng, autostep))
1020 except RangeSetParseError, ex:
1021 raise NodeSetParseRangeError(ex)
1022
1023
1024 if len(pfx) + len(sfx) == 0:
1025 msg = "fully numeric nodeset"
1026 raise NodeSetParseError(nsstr, msg)
1027
1028
1029 op_idx, next_op_code = self._next_op(sfx)
1030 if op_idx < 0:
1031 nsstr = None
1032 else:
1033 opc = self.OP_CODES[next_op_code]
1034 sfx, nsstr = sfx.split(opc, 1)
1035
1036 if not nsstr:
1037 msg = "missing nodeset operand with '%s' operator" % opc
1038 raise NodeSetParseError(None, msg)
1039
1040
1041 sfx = sfx.rstrip()
1042 if sfx:
1043 sfx, sfxrvec = self._scan_string_single(sfx, autostep)
1044 newpat += sfx
1045 rsets += sfxrvec
1046 else:
1047
1048
1049
1050 if op_idx < 0:
1051 node = nsstr
1052 nsstr = None
1053 else:
1054 opc = self.OP_CODES[next_op_code]
1055 node, nsstr = nsstr.split(opc, 1)
1056
1057 if not node or not nsstr:
1058 msg = "missing nodeset operand with '%s' operator" % opc
1059 raise NodeSetParseError(node or nsstr, msg)
1060
1061
1062 if node.find(self.BRACKET_CLOSE) > -1:
1063 raise NodeSetParseError(node, "illegal closing bracket")
1064
1065
1066 node = node.rstrip()
1067 newpat, rsets = self._scan_string_single(node, autostep)
1068
1069 if len(rsets) > 1:
1070 yield op_code, newpat, RangeSetND([rsets], None, autostep,
1071 copy_rangeset=False)
1072 elif len(rsets) == 1:
1073 yield op_code, newpat, rsets[0]
1074 else:
1075 yield op_code, newpat, None
1076
1078 """Helper to get rid of leading bracket digits.
1079
1080 Take a bracket outer prefix string and an inner range set string and
1081 return amended strings.
1082 """
1083 outerstrip = outer.rstrip(string.digits)
1084 outerlen, outerstriplen = len(outer), len(outerstrip)
1085 if outerstriplen < outerlen:
1086
1087 outerdigits = outer[outerstriplen:]
1088 inner = ','.join(
1089 '-'.join(outerdigits + bound for bound in elem.split('-'))
1090 for elem in (str(subrng)
1091 for subrng in RangeSet(inner).contiguous()))
1092 return outerstrip, inner
1093
1095 """Helper to get rid of trailing bracket digits.
1096
1097 Take a bracket outer suffix string and an inner range set string and
1098 return amended strings.
1099 """
1100 outerstrip = outer.lstrip(string.digits)
1101 outerlen, outerstriplen = len(outer), len(outerstrip)
1102 if outerstriplen < outerlen:
1103
1104 if '/' in inner:
1105 msg = "illegal trailing digits after range with steps"
1106 raise NodeSetParseError(outer, msg)
1107
1108 outerdigits = outer[0:outerlen-outerstriplen]
1109 outlen = len(outerdigits)
1110 def shiftstep(orig, power):
1111 """Add needed step after shifting range indexes"""
1112 if '-' in orig:
1113 return orig + '/1' + '0' * power
1114 return orig
1115 inner = ','.join(shiftstep(s, outlen) for s in
1116 ('-'.join(bound + outerdigits
1117 for bound in elem.split('-'))
1118 for elem in inner.split(',')))
1119 return outerstrip, inner
1120
1122 """
1123 Iterable class of nodes with node ranges support.
1124
1125 NodeSet creation examples:
1126
1127 >>> nodeset = NodeSet() # empty NodeSet
1128 >>> nodeset = NodeSet("cluster3") # contains only cluster3
1129 >>> nodeset = NodeSet("cluster[5,10-42]")
1130 >>> nodeset = NodeSet("cluster[0-10/2]")
1131 >>> nodeset = NodeSet("cluster[0-10/2],othername[7-9,120-300]")
1132
1133 NodeSet provides methods like update(), intersection_update() or
1134 difference_update() methods, which conform to the Python Set API.
1135 However, unlike RangeSet or standard Set, NodeSet is somewhat not
1136 so strict for convenience, and understands NodeSet instance or
1137 NodeSet string as argument. Also, there is no strict definition of
1138 one element, for example, it IS allowed to do:
1139
1140 >>> nodeset = NodeSet("blue[1-50]")
1141 >>> nodeset.remove("blue[36-40]")
1142 >>> print nodeset
1143 blue[1-35,41-50]
1144
1145 Additionally, the NodeSet class recognizes the "extended string
1146 pattern" which adds support for union (special character ","),
1147 difference ("!"), intersection ("&") and symmetric difference ("^")
1148 operations. String patterns are read from left to right, by
1149 proceeding any character operators accordinately.
1150
1151 Extended string pattern usage examples:
1152
1153 >>> nodeset = NodeSet("node[0-10],node[14-16]") # union
1154 >>> nodeset = NodeSet("node[0-10]!node[8-10]") # difference
1155 >>> nodeset = NodeSet("node[0-10]&node[5-13]") # intersection
1156 >>> nodeset = NodeSet("node[0-10]^node[5-13]") # xor
1157 """
1158
1159 _VERSION = 2
1160
1161 - def __init__(self, nodes=None, autostep=None, resolver=None,
1162 fold_axis=None):
1163 """Initialize a NodeSet object.
1164
1165 The `nodes` argument may be a valid nodeset string or a NodeSet
1166 object. If no nodes are specified, an empty NodeSet is created.
1167
1168 The optional `autostep` argument is passed to underlying
1169 :class:`.RangeSet.RangeSet` objects and aims to enable and make use of
1170 the range/step syntax (eg. ``node[1-9/2]``) when converting NodeSet to
1171 string (using folding). To enable this feature, autostep must be set
1172 there to the min number of indexes that are found at equal distance of
1173 each other inside a range before NodeSet starts to use this syntax. For
1174 example, `autostep=3` (or less) will pack ``n[2,4,6]`` into
1175 ``n[2-6/2]``. Default autostep value is None which means "inherit
1176 whenever possible", ie. do not enable it unless set in NodeSet objects
1177 passed as `nodes` here or during arithmetic operations.
1178 You may however use the special ``AUTOSTEP_DISABLED`` constant to force
1179 turning off autostep feature.
1180
1181 The optional `resolver` argument may be used to override the group
1182 resolving behavior for this NodeSet object. It can either be set to a
1183 :class:`.NodeUtils.GroupResolver` object, to the ``RESOLVER_NOGROUP``
1184 constant to disable any group resolution, or to None (default) to use
1185 standard NodeSet group resolver (see :func:`.set_std_group_resolver()`
1186 at the module level to change it if needed).
1187
1188 nD nodeset only: the optional `fold_axis` parameter, if specified, set
1189 the public instance member `fold_axis` to an iterable over nD 0-indexed
1190 axis integers. This parameter may be used to disengage some nD folding.
1191 That may be useful as all cluster tools don't support folded-nD nodeset
1192 syntax. Pass ``[0]``, for example, to only fold along first axis (that
1193 is, to fold first dimension using ``[a-b]`` rangeset syntax whenever
1194 possible). Using `fold_axis` ensures that rangeset won't be folded on
1195 unspecified axis, but please note however, that using `fold_axis` may
1196 lead to suboptimial folding, this is because NodeSet algorithms are
1197 optimized for folding along all axis (default behavior).
1198 """
1199 NodeSetBase.__init__(self, autostep=autostep, fold_axis=fold_axis)
1200
1201
1202 if resolver in (RESOLVER_NOGROUP, RESOLVER_NOINIT):
1203 self._resolver = None
1204 else:
1205 self._resolver = resolver or RESOLVER_STD_GROUP
1206
1207
1208 if resolver == RESOLVER_NOINIT:
1209 self._parser = None
1210 else:
1211 self._parser = ParsingEngine(self._resolver)
1212 self.update(nodes)
1213
1214 @classmethod
1215 - def _fromlist1(cls, nodelist, autostep=None, resolver=None):
1216 """Class method that returns a new NodeSet with single nodes from
1217 provided list (optimized constructor)."""
1218 inst = NodeSet(autostep=autostep, resolver=resolver)
1219 for single in nodelist:
1220 inst.update(inst._parser.parse_string_single(single, autostep))
1221 return inst
1222
1223 @classmethod
1224 - def fromlist(cls, nodelist, autostep=None, resolver=None):
1225 """Class method that returns a new NodeSet with nodes from provided
1226 list."""
1227 inst = NodeSet(autostep=autostep, resolver=resolver)
1228 inst.updaten(nodelist)
1229 return inst
1230
1231 @classmethod
1232 - def fromall(cls, groupsource=None, autostep=None, resolver=None):
1247
1249 """Called when pickling: remove references to group resolver."""
1250 odict = self.__dict__.copy()
1251 odict['_version'] = NodeSet._VERSION
1252 del odict['_resolver']
1253 del odict['_parser']
1254 return odict
1255
1257 """Called when unpickling: restore parser using non group
1258 resolver."""
1259 self.__dict__.update(dic)
1260 self._resolver = None
1261 self._parser = ParsingEngine(None)
1262 if getattr(self, '_version', 1) <= 1:
1263 self.fold_axis = None
1264
1265
1266 old_patterns = self._patterns
1267 self._patterns = {}
1268 for pat, rangeset in sorted(old_patterns.iteritems()):
1269 if rangeset:
1270 assert isinstance(rangeset, RangeSet)
1271 rgs = str(rangeset)
1272 if len(rangeset) > 1:
1273 rgs = "[%s]" % rgs
1274 self.update(pat % rgs)
1275 else:
1276 self.update(pat)
1277
1279 """Return a shallow copy of a NodeSet."""
1280 cpy = self.__class__(resolver=RESOLVER_NOINIT)
1281 dic = {}
1282 for pat, rangeset in self._patterns.iteritems():
1283 if rangeset is None:
1284 dic[pat] = None
1285 else:
1286 dic[pat] = rangeset.copy()
1287 cpy._patterns = dic
1288 cpy.fold_axis = self.fold_axis
1289 cpy._autostep = self._autostep
1290 cpy._resolver = self._resolver
1291 cpy._parser = self._parser
1292 return cpy
1293
1294 __copy__ = copy
1295
1311
1312 - def _groups2(self, groupsource=None, autostep=None):
1313 """Find node groups this nodeset belongs to. [private]"""
1314 if not self._resolver:
1315 raise NodeSetExternalError("No node group resolver")
1316 try:
1317
1318 allgrplist = self._parser.grouplist(groupsource)
1319 except NodeUtils.GroupSourceError:
1320
1321
1322 allgrplist = None
1323 groups_info = {}
1324 allgroups = {}
1325
1326
1327
1328 if self._resolver.has_node_groups(groupsource) and \
1329 (not allgrplist or len(allgrplist) >= len(self)):
1330
1331 pass
1332 else:
1333 if not allgrplist:
1334 return groups_info
1335 try:
1336
1337 for grp in allgrplist:
1338 nodelist = self._resolver.group_nodes(grp, groupsource)
1339 allgroups[grp] = NodeSet(",".join(nodelist),
1340 resolver=self._resolver)
1341 except NodeUtils.GroupSourceQueryFailed, exc:
1342
1343 raise NodeSetExternalError("Unable to map a group " \
1344 "previously listed\n\tFailed command: %s" % exc)
1345
1346
1347 for node in self._iterbase():
1348 for grp in self._find_groups(node, groupsource, allgroups):
1349 if grp not in groups_info:
1350 nodes = self._parser.parse_group(grp, groupsource, autostep)
1351 groups_info[grp] = (1, nodes)
1352 else:
1353 i, nodes = groups_info[grp]
1354 groups_info[grp] = (i + 1, nodes)
1355 return groups_info
1356
1357 - def groups(self, groupsource=None, noprefix=False):
1358 """Find node groups this nodeset belongs to.
1359
1360 Return a dictionary of the form:
1361 group_name => (group_nodeset, contained_nodeset)
1362
1363 Group names are always prefixed with "@". If groupsource is provided,
1364 they are prefixed with "@groupsource:", unless noprefix is True.
1365 """
1366 groups = self._groups2(groupsource, self._autostep)
1367 result = {}
1368 for grp, (_, nsb) in groups.iteritems():
1369 if groupsource and not noprefix:
1370 key = "@%s:%s" % (groupsource, grp)
1371 else:
1372 key = "@" + grp
1373 result[key] = (NodeSet(nsb, resolver=self._resolver),
1374 self.intersection(nsb))
1375 return result
1376
1377 - def regroup(self, groupsource=None, autostep=None, overlap=False,
1378 noprefix=False):
1379 """Regroup nodeset using node groups.
1380
1381 Try to find fully matching node groups (within specified groupsource)
1382 and return a string that represents this node set (containing these
1383 potential node groups). When no matching node groups are found, this
1384 method returns the same result as str()."""
1385 groups = self._groups2(groupsource, autostep)
1386 if not groups:
1387 return str(self)
1388
1389
1390 fulls = []
1391 for k, (i, nodes) in groups.iteritems():
1392 assert i <= len(nodes)
1393 if i == len(nodes):
1394 fulls.append((i, k))
1395
1396 rest = NodeSet(self, resolver=RESOLVER_NOGROUP)
1397 regrouped = NodeSet(resolver=RESOLVER_NOGROUP)
1398
1399 bigalpha = lambda x, y: cmp(y[0], x[0]) or cmp(x[1], y[1])
1400
1401
1402 for _, grp in sorted(fulls, cmp=bigalpha):
1403 if not overlap and groups[grp][1] not in rest:
1404 continue
1405 if groupsource and not noprefix:
1406 regrouped.update("@%s:%s" % (groupsource, grp))
1407 else:
1408 regrouped.update("@" + grp)
1409 rest.difference_update(groups[grp][1])
1410 if not rest:
1411 return str(regrouped)
1412
1413 if regrouped:
1414 return "%s,%s" % (regrouped, rest)
1415
1416 return str(rest)
1417
1424
1431
1433 """
1434 Return the node at specified index or a subnodeset when a slice
1435 is specified.
1436 """
1437 base = NodeSetBase.__getitem__(self, index)
1438 if not isinstance(base, NodeSetBase):
1439 return base
1440
1441 inst = NodeSet(autostep=self._autostep, resolver=self._resolver)
1442 inst._patterns = base._patterns
1443 return inst
1444
1446 """
1447 Split the nodeset into nbr sub-nodesets (at most). Each
1448 sub-nodeset will have the same number of elements more or
1449 less 1. Current nodeset remains unmodified.
1450
1451 >>> for nodeset in NodeSet("foo[1-5]").split(3):
1452 ... print nodeset
1453 foo[1-2]
1454 foo[3-4]
1455 foo5
1456 """
1457 assert(nbr > 0)
1458
1459
1460 slice_size = len(self) / nbr
1461 left = len(self) % nbr
1462
1463 begin = 0
1464 for i in range(0, min(nbr, len(self))):
1465 length = slice_size + int(i < left)
1466 yield self[begin:begin + length]
1467 begin += length
1468
1475
1483
1485 """
1486 s.difference_update(t) removes from s all the elements
1487 found in t. If strict is True, raise KeyError if an
1488 element in t cannot be removed from s.
1489 """
1490 nodeset = self._parser.parse(other, self._autostep)
1491 NodeSetBase.difference_update(self, nodeset, strict)
1492
1500
1503 """
1504 Commodity function that expands a nodeset pattern into a list of nodes.
1505 """
1506 return list(NodeSet(pat))
1507
1509 """
1510 Commodity function that clean dups and fold provided pattern with ranges
1511 and "/step" support.
1512 """
1513 return str(NodeSet(pat))
1514
1515 -def grouplist(namespace=None, resolver=None):
1516 """
1517 Commodity function that retrieves the list of raw groups for a specified
1518 group namespace (or use default namespace).
1519 Group names are not prefixed with "@".
1520 """
1521 return ParsingEngine(resolver or RESOLVER_STD_GROUP).grouplist(namespace)
1522
1524 """
1525 Get the current resolver used for standard "@" group resolution.
1526 """
1527 return RESOLVER_STD_GROUP
1528
1530 """
1531 Override the resolver used for standard "@" group resolution. The
1532 new resolver should be either an instance of
1533 NodeUtils.GroupResolver or None. In the latter case, the group
1534 resolver is restored to the default one.
1535 """
1536 global RESOLVER_STD_GROUP
1537 RESOLVER_STD_GROUP = new_resolver or _DEF_RESOLVER_STD_GROUP
1538