1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 """
36 Cluster node set module.
37
38 A module to deal efficiently with 1D rangesets and nodesets (pdsh-like).
39 Instances of RangeSet and NodeSet both provide similar operations than
40 the builtin set() type and Set object.
41 See http://www.python.org/doc/lib/set-objects.html
42
43 Usage example
44 =============
45 >>> # Import NodeSet class
46 ... from ClusterShell.NodeSet import NodeSet
47 >>>
48 >>> # Create a new nodeset from string
49 ... nodeset = NodeSet("cluster[1-30]")
50 >>> # Add cluster32 to nodeset
51 ... nodeset.update("cluster32")
52 >>> # Remove from nodeset
53 ... nodeset.difference_update("cluster[2-5]")
54 >>> # Print nodeset as a pdsh-like pattern
55 ... print nodeset
56 cluster[1,6-30,32]
57 >>> # Iterate over node names in nodeset
58 ... for node in nodeset:
59 ... print node
60 [...]
61 """
62
63 import copy
64 import re
65
66 import ClusterShell.NodeUtils as NodeUtils
67
68
69
70 DEF_GROUPS_CONFIG = "/etc/clustershell/groups.conf"
71 DEF_STD_GROUP_RESOLVER = NodeUtils.GroupResolverConfig(DEF_GROUPS_CONFIG)
72 STD_GROUP_RESOLVER = DEF_STD_GROUP_RESOLVER
76 """Base RangeSet exception class."""
77
79 """Raised when RangeSet parsing cannot be done properly."""
86
88 """Raised when a fatal padding incoherency occurs"""
91
94 """Base NodeSet exception class."""
95
97 """Raised when NodeSet parsing cannot be done properly."""
104
106 """Raised when bad range is encountered during NodeSet parsing."""
109
110 -class NodeSetExternalError(NodeSetException):
111 """Raised when an external error is encountered."""
112
115 """
116 Advanced range sets.
117
118 RangeSet creation examples:
119 >>> rset = RangeSet() # empty RangeSet
120 >>> rset = RangeSet("5,10-42") # contains 5, 10 to 42
121 >>> rset = RangeSet("0-10/2") # contains 0, 2, 4, 6, 8, 10
122
123 Also, RangeSet provides methods like update(), intersection_update()
124 or difference_update(), which conform to the Python Set API.
125 """
126 - def __init__(self, pattern=None, autostep=None):
127 """
128 Initialize RangeSet with optional pdsh-like string pattern and
129 autostep threshold.
130 """
131 if autostep is None:
132
133
134 self._autostep = 1E100
135 else:
136
137
138 self._autostep = int(autostep) - 1
139
140 self._length = 0
141 self._ranges = []
142
143 if pattern is not None:
144
145
146 if pattern.find(',') < 0:
147 subranges = [pattern]
148 else:
149 subranges = pattern.split(',')
150
151 for subrange in subranges:
152 if subrange.find('/') < 0:
153 step = 1
154 baserange = subrange
155 else:
156 baserange, step = subrange.split('/', 1)
157
158 try:
159 step = int(step)
160 except ValueError:
161 raise RangeSetParseError(subrange,
162 "cannot convert string to integer")
163
164 if baserange.find('-') < 0:
165 if step != 1:
166 raise RangeSetParseError(subrange,
167 "invalid step usage")
168 begin = end = baserange
169 else:
170 begin, end = baserange.split('-', 1)
171
172
173 try:
174 pad = 0
175 if int(begin) != 0:
176 begins = begin.lstrip("0")
177 if len(begin) - len(begins) > 0:
178 pad = len(begin)
179 start = int(begins)
180 else:
181 if len(begin) > 1:
182 pad = len(begin)
183 start = 0
184 if int(end) != 0:
185 ends = end.lstrip("0")
186 else:
187 ends = end
188 stop = int(ends)
189 except ValueError:
190 raise RangeSetParseError(subrange,
191 "cannot convert string to integer")
192
193
194 if start > stop or step < 1:
195 raise RangeSetParseError(subrange,
196 "invalid values in range")
197
198 self.add_range(start, stop, step, pad)
199
200 @classmethod
201 - def fromlist(cls, rglist, autostep=None):
202 """
203 Class method that returns a new RangeSet with ranges from
204 provided list.
205 """
206 inst = RangeSet(autostep=autostep)
207 for rg in rglist:
208 if isinstance(rg, RangeSet):
209 inst.update(rg)
210 else:
211 inst.update(RangeSet(rg))
212 return inst
213
214 @classmethod
215 - def fromone(cls, index, pad=0, autostep=None):
216 """
217 Class method that returns a new RangeSet of one single item.
218 """
219 inst = RangeSet(autostep=autostep)
220 inst.add(index, pad)
221 return inst
222
224 """
225 Iterate over each item in RangeSet.
226 """
227 for start, stop, step, pad in self._ranges:
228 for i in range(start, stop + 1, step):
229 yield "%*d" % (pad, i)
230
232 """
233 Get the number of items in RangeSet.
234 """
235 return self._length
236
238 """
239 Get range-based string.
240 """
241 cnt = 0
242 s = ""
243 for start, stop, step, pad in self._ranges:
244 assert pad != None
245 if cnt > 0:
246 s += ","
247 if start == stop:
248 s += "%0*d" % (pad, start)
249 else:
250 assert step >= 0, "Internal error: step < 0"
251 if step == 1:
252 s += "%0*d-%0*d" % (pad, start, pad, stop)
253 else:
254 s += "%0*d-%0*d/%d" % (pad, start, pad, stop, step)
255 cnt += stop - start + 1
256 return s
257
258
259
260 __repr__ = __str__
261
263 """
264 Is element contained in RangeSet? Element can be either a
265 string with optional padding (eg. "002") or an integer
266 (obviously, no padding check is performed for integer).
267 """
268
269 if type(elem) is str:
270 pad = 0
271 if int(elem) != 0:
272 selem = elem.lstrip("0")
273 if len(elem) - len(selem) > 0:
274 pad = len(elem)
275 ielem = int(selem)
276 else:
277 if len(elem) > 1:
278 pad = len(elem)
279 ielem = 0
280 return self._contains_with_padding(ielem, pad)
281
282
283 return self._contains(int(elem))
284
286 """
287 Contains subroutine that takes an integer.
288 """
289 for rgstart, rgstop, rgstep, rgpad in self._ranges:
290 if ielem >= rgstart and ielem <= rgstop and \
291 (ielem - rgstart) % rgstep == 0:
292 return True
293 return False
294
296 """
297 Contains subroutine that takes an integer and a padding value.
298 """
299 for rgstart, rgstop, rgstep, rgpad in self._ranges:
300
301
302 if ielem >= rgstart and ielem <= rgstop and \
303 (pad == rgpad or (pad == 0 and len(str(ielem)) >= rgpad)) and \
304 (ielem - rgstart) % rgstep == 0:
305 return True
306 return False
307
309
310
311 if not isinstance(other, RangeSet):
312 raise TypeError, "Binary operation only permitted between RangeSets"
313
315 """
316 Report whether another rangeset contains this rangeset.
317 """
318 self._binary_sanity_check(rangeset)
319 for start, stop, step, pad in self._ranges:
320 for i in range(start, stop + 1, step):
321 if not rangeset._contains_with_padding(i, pad):
322 return False
323 return True
324
326 """
327 Report whether this rangeset contains another rangeset.
328 """
329 self._binary_sanity_check(rangeset)
330 return rangeset.issubset(self)
331
333 """
334 RangeSet equality comparison.
335 """
336
337
338
339
340
341 if not isinstance(other, RangeSet):
342 return NotImplemented
343 return len(self) == len(other) and self.issubset(other)
344
345
346 __le__ = issubset
347 __ge__ = issuperset
348
355
362
364 """
365 Return the element at index i.
366 """
367 length = 0
368 for start, stop, step, pad in self._ranges:
369 cnt = (stop - start) / step + 1
370 if i < length + cnt:
371 return start + (i - length) * step
372 length += cnt
373
375 """
376 Return the slice from index i to index j-1. For convenience
377 only, not optimized as of version 1.0.
378 """
379 return RangeSet.fromlist(list(self)[i:j])
380
382 """
383 Split the rangeset into nbr sub-rangeset. Each sub-rangeset will have
384 the same number of element more or less 1. Current rangeset remains
385 unmodified. Returns an iterator.
386
387 >>> RangeSet("1-5").split(3)
388 RangeSet("1-2")
389 RangeSet("3-4")
390 RangeSet("foo5")
391 """
392
393 assert(nbr > 0)
394
395
396 slice_size = len(self) / nbr
397 left = len(self) % nbr
398
399 begin = 0
400 for i in range(0, nbr):
401 length = slice_size + int(i < left)
402 yield self[begin:begin + length]
403 begin += length
404
405
407 """
408 Expand all items. Internal use.
409 """
410 items = []
411 pad = 0
412 for rgstart, rgstop, rgstep, rgpad in self._ranges:
413 items += range(rgstart, rgstop + 1, rgstep)
414 pad = pad or rgpad
415 return items, pad
416
417 - def _fold(self, items, pad):
418 """
419 Fold items as ranges and group them by step.
420 Return: (ranges, total_length)
421 """
422 cnt, k, m, istart, rg = 0, -1, 0, None, []
423
424
425 for i in items:
426 if i > k:
427 cnt += 1
428 if istart is None:
429 istart = k = i
430 elif m > 0:
431 if m != i - k:
432 if m == 1 or k - istart >= self._autostep * m:
433
434 rg.append((istart, k, m, pad))
435 istart = k = i
436 elif k - istart > m:
437
438
439 for j in range(istart, k, m):
440 rg.append((j, j, 1, pad))
441 istart = k
442 else:
443 rg.append((istart, istart, 1, pad))
444 istart = k
445 m = i - k
446 k = i
447
448
449 if istart is not None:
450 if m > 0:
451 if m == 1 or k - istart >= self._autostep * m:
452
453 rg.append((istart, k, m, pad))
454 elif k - istart > m:
455
456 for j in range(istart, k + m, m):
457 rg.append((j, j, 1, pad))
458 else:
459 rg.append((istart, istart, 1, pad))
460 rg.append((k, k, 1, pad))
461 else:
462 rg.append((istart, istart, 1, pad))
463
464 return rg, cnt
465
466 - def add_range(self, start, stop, step=1, pad=0):
467 """
468 Add a range (start, stop, step and padding length) to RangeSet.
469 """
470 assert start <= stop, "please provide ordered node index ranges"
471 assert step != None
472 assert step > 0
473 assert pad != None
474 assert pad >= 0
475 assert stop - start < 1e9, "range too large"
476
477 if self._length == 0:
478
479 stop_adjust = stop - (stop - start) % step
480 if step == 1 or stop_adjust - start >= self._autostep * step:
481 self._ranges = [ (start, stop_adjust, step, pad) ]
482 else:
483 for j in range(start, stop_adjust + step, step):
484 self._ranges.append((j, j, step, pad))
485 self._length = (stop_adjust - start) / step + 1
486 else:
487 self._ranges, self._length = self._add_range_exfold(start, stop, \
488 step, pad)
489
491 """
492 Add range expanding then folding all items.
493 """
494 assert start <= stop, "please provide ordered node index ranges"
495 assert step > 0
496 assert pad != None
497 assert pad >= 0
498
499 items, rgpad = self._expand()
500 items += range(start, stop + 1, step)
501 items.sort()
502
503 return self._fold(items, pad or rgpad)
504
506 """
507 s.union(t) returns a new rangeset with elements from both s and t.
508 """
509 self_copy = copy.deepcopy(self)
510 self_copy.update(other)
511 return self_copy
512
514 """
515 Implements the | operator. So s | t returns a new rangeset with
516 elements from both s and t.
517 """
518 if not isinstance(other, RangeSet):
519 return NotImplemented
520 return self.union(other)
521
522 - def add(self, elem, pad=0):
523 """
524 Add element to RangeSet.
525 """
526 self.add_range(elem, elem, step=1, pad=pad)
527
529 """
530 Update a rangeset with the union of itself and another.
531 """
532 for start, stop, step, pad in rangeset._ranges:
533 self.add_range(start, stop, step, pad)
534
536 """
537 Remove all ranges from this rangeset.
538 """
539 self._ranges = []
540 self._length = 0
541
543 """
544 Implements the |= operator. So s |= t returns rangeset s with
545 elements added from t. (Python version 2.5+ required)
546 """
547 self._binary_sanity_check(other)
548 return self.update(other)
549
551 """
552 s.intersection(t) returns a new rangeset with elements common
553 to s and t.
554 """
555 self_copy = copy.deepcopy(self)
556 self_copy.intersection_update(rangeset)
557 return self_copy
558
560 """
561 Implements the & operator. So s & t returns a new rangeset with
562 elements common to s and t.
563 """
564 if not isinstance(other, RangeSet):
565 return NotImplemented
566 return self.intersection(other)
567
569 """
570 Intersection with provided RangeSet.
571 """
572 self._ranges, self._length = self._intersect_exfold(rangeset)
573
575 """
576 Implements the &= operator. So s &= t returns rangeset s keeping
577 only elements also found in t. (Python version 2.5+ required)
578 """
579 self._binary_sanity_check(other)
580 return self.intersection_update(other)
581
583 """
584 Calc intersection with the expand/fold method.
585 """
586
587 items1, pad1 = self._expand()
588 items2, pad2 = rangeset._expand()
589
590
591 iset = dict.fromkeys(items2)
592
593
594 return self._fold([e for e in items1 if e in iset], pad1 or pad2)
595
597 """
598 s.difference(t) returns a new rangeset with elements in s but
599 not in t.
600 in t.
601 """
602 self_copy = copy.deepcopy(self)
603 self_copy.difference_update(rangeset)
604 return self_copy
605
607 """
608 Implement the - operator. So s - t returns a new rangeset with
609 elements in s but not in t.
610 """
611 if not isinstance(other, RangeSet):
612 return NotImplemented
613 return self.difference(other)
614
616 """
617 s.difference_update(t) returns rangeset s after removing
618 elements found in t. If strict is True, raise KeyError
619 if an element cannot be removed.
620 """
621 self._ranges, self._length = self._sub_exfold(rangeset, strict)
622
624 """
625 Implement the -= operator. So s -= t returns rangeset s after
626 removing elements found in t. (Python version 2.5+ required)
627 """
628 self._binary_sanity_check(other)
629 return self.difference_update(other)
630
632 """
633 Remove element elem from the RangeSet. Raise KeyError if elem
634 is not contained in the RangeSet.
635 """
636 items1, pad1 = self._expand()
637
638 try:
639 items1.remove(elem)
640 except ValueError, e:
641 raise KeyError, elem
642
643 self._ranges, self._length = self._fold(items1, pad1)
644
646 """
647 Calc sub/exclusion with the expand/fold method. If strict is
648 True, raise KeyError if the rangeset is not included.
649 """
650
651 items1, pad1 = self._expand()
652 items2, pad2 = rangeset._expand()
653
654
655 iset = dict.fromkeys(items2)
656
657 if strict:
658
659 lst = []
660 for e in items1:
661 if e not in iset:
662 lst.append(e)
663 else:
664 del iset[e]
665
666
667 if len(iset) > 0:
668
669
670 missing = RangeSet()
671 missing._ranges, missing._length = self._fold(iset.keys(), pad2)
672
673 raise KeyError, missing
674
675 return self._fold(lst, pad1 or pad2)
676 else:
677
678 return self._fold([e for e in items1 if e not in iset],
679 pad1 or pad2)
680
682 """
683 s.symmetric_difference(t) returns the symmetric difference of
684 two rangesets as a new RangeSet.
685
686 (ie. all elements that are in exactly one of the rangesets.)
687 """
688 self_copy = copy.deepcopy(self)
689 self_copy.symmetric_difference_update(other)
690 return self_copy
691
693 """
694 Implement the ^ operator. So s ^ t returns a new rangeset with
695 elements that are in exactly one of the rangesets.
696 """
697 if not isinstance(other, RangeSet):
698 return NotImplemented
699 return self.symmetric_difference(other)
700
702 """
703 s.symmetric_difference_update(t) returns rangeset s keeping all
704 elements that are in exactly one of the rangesets.
705 """
706 self._ranges, self._length = self._xor_exfold(rangeset)
707
709 """
710 Implement the ^= operator. So s ^= t returns rangeset s after
711 keeping all elements that are in exactly one of the rangesets.
712 (Python version 2.5+ required)
713 """
714 self._binary_sanity_check(other)
715 return self.symmetric_difference_update(other)
716
718 """
719 Calc symmetric difference (xor).
720 """
721
722 items1, pad1 = self._expand()
723 items2, pad2 = rangeset._expand()
724
725 if pad1 != pad2:
726 raise RangeSetPaddingError('', "%s != %s" % (pad1, pad2))
727
728
729
730 iset1 = dict.fromkeys(items1)
731 iset2 = dict.fromkeys(items2)
732
733
734 allitems = items1 + items2
735 lst = [e for e in allitems if not e in iset1 or not e in iset2]
736 lst.sort()
737
738 return self._fold(lst, pad1)
739
742 """
743 Base class for NodeSet.
744 """
745 - def __init__(self, pattern=None, rangeset=None):
746 """
747 Initialize an empty NodeSetBase.
748 """
749 self._length = 0
750 self._patterns = {}
751 if pattern:
752 self._add(pattern, rangeset)
753 elif rangeset:
754 raise ValueError("missing pattern")
755
757 """
758 Iterator on internal item tuples (pattern, index, padding).
759 """
760 for pat, rangeset in sorted(self._patterns.iteritems()):
761 if rangeset:
762 for start, stop, step, pad in rangeset._ranges:
763 while start <= stop:
764 yield pat, start, pad
765 start += step
766 else:
767 yield pat, None, None
768
770 """
771 Iterator on single, one-item NodeSetBase objects.
772 """
773 for pat, start, pad in self._iter():
774 if start is not None:
775 yield NodeSetBase(pat, RangeSet.fromone(start, pad))
776 else:
777 yield NodeSetBase(pat)
778
780 """
781 Iterator on single nodes as string.
782 """
783
784 for pat, start, pad in self._iter():
785 if start is not None:
786 yield pat % ("%0*d" % (pad, start))
787 else:
788 yield pat
789
791 """
792 Get the number of nodes in NodeSet.
793 """
794 cnt = 0
795 for rangeset in self._patterns.itervalues():
796 if rangeset:
797 cnt += len(rangeset)
798 else:
799 cnt += 1
800 return cnt
801
803 """
804 Get ranges-based pattern of node list.
805 """
806 result = ""
807 for pat, rangeset in sorted(self._patterns.iteritems()):
808 if rangeset:
809 s = str(rangeset)
810 cnt = len(rangeset)
811 if cnt > 1:
812 s = "[" + s + "]"
813 result += pat % s
814 else:
815 result += pat
816 result += ","
817 return result[:-1]
818
820 """
821 Is node contained in NodeSet ?
822 """
823 return self.issuperset(other)
824
826
827
828 if not isinstance(other, NodeSetBase):
829 raise TypeError, "Binary operation only permitted between NodeSetBase"
830
837
839 """
840 Report whether this nodeset contains another nodeset.
841 """
842 self._binary_sanity_check(other)
843 status = True
844 for pat, erangeset in other._patterns.iteritems():
845 rangeset = self._patterns.get(pat)
846 if rangeset:
847 status = rangeset.issuperset(erangeset)
848 else:
849
850 status = self._patterns.has_key(pat)
851 if not status:
852 break
853 return status
854
856 """
857 NodeSet equality comparison.
858 """
859
860 if not isinstance(other, NodeSetBase):
861 return NotImplemented
862 return len(self) == len(other) and self.issuperset(other)
863
864
865 __le__ = issubset
866 __ge__ = issuperset
867
874
881
883 """
884 Return the node at index i. For convenience only, not
885 optimized as of version 1.0.
886 """
887 return list(self)[i]
888
889 - def _add(self, pat, rangeset):
890 """
891 Add nodes from a (pat, rangeset) tuple. `pat' may be an existing
892 pattern and `rangeset' may be None.
893 """
894
895 pat_e = self._patterns.get(pat)
896
897 if pat_e:
898
899
900 assert rangeset != None
901
902
903 pat_e.update(rangeset)
904 else:
905
906 self._patterns[pat] = copy.copy(rangeset)
907
909 """
910 s.union(t) returns a new set with elements from both s and t.
911 """
912 self_copy = copy.deepcopy(self)
913 self_copy.update(other)
914 return self_copy
915
917 """
918 Implements the | operator. So s | t returns a new nodeset with
919 elements from both s and t.
920 """
921 if not isinstance(other, NodeSetBase):
922 return NotImplemented
923 return self.union(other)
924
925 - def add(self, other):
926 """
927 Add node to NodeSet.
928 """
929 self.update(other)
930
932 """
933 s.update(t) returns nodeset s with elements added from t.
934 """
935 self._binary_sanity_check(other)
936
937 for pat, rangeset in other._patterns.iteritems():
938 self._add(pat, rangeset)
939
941 """
942 Remove all nodes from this nodeset.
943 """
944 self._patterns.clear()
945 self._length = 0
946
948 """
949 Implements the |= operator. So s |= t returns nodeset s with
950 elements added from t. (Python version 2.5+ required)
951 """
952 self._binary_sanity_check(other)
953 return self.update(other)
954
956 """
957 s.intersection(t) returns a new set with elements common to s
958 and t.
959 """
960 self_copy = copy.deepcopy(self)
961 self_copy.intersection_update(other)
962 return self_copy
963
965 """
966 Implements the & operator. So s & t returns a new nodeset with
967 elements common to s and t.
968 """
969 if not isinstance(other, NodeSet):
970 return NotImplemented
971 return self.intersection(other)
972
974 """
975 s.intersection_update(t) returns nodeset s keeping only
976 elements also found in t.
977 """
978 self._binary_sanity_check(other)
979
980 if other is self:
981 return
982
983 tmp_ns = NodeSetBase()
984
985 for pat, irangeset in other._patterns.iteritems():
986 rangeset = self._patterns.get(pat)
987 if rangeset:
988 rs = copy.copy(rangeset)
989 rs.intersection_update(irangeset)
990
991 if len(rs) > 0:
992 tmp_ns._add(pat, rs)
993 elif not irangeset and pat in self._patterns:
994
995 tmp_ns._add(pat, None)
996 elif not irangeset and pat in self._patterns:
997
998 tmp_ns._add(pat, None)
999
1000
1001 self._patterns = tmp_ns._patterns
1002
1004 """
1005 Implements the &= operator. So s &= t returns nodeset s keeping
1006 only elements also found in t. (Python version 2.5+ required)
1007 """
1008 self._binary_sanity_check(other)
1009 return self.intersection_update(other)
1010
1012 """
1013 s.difference(t) returns a new NodeSet with elements in s but not
1014 in t.
1015 """
1016 self_copy = copy.deepcopy(self)
1017 self_copy.difference_update(other)
1018 return self_copy
1019
1021 """
1022 Implement the - operator. So s - t returns a new nodeset with
1023 elements in s but not in t.
1024 """
1025 if not isinstance(other, NodeSetBase):
1026 return NotImplemented
1027 return self.difference(other)
1028
1030 """
1031 s.difference_update(t) returns nodeset s after removing
1032 elements found in t. If strict is True, raise KeyError
1033 if an element cannot be removed.
1034 """
1035 self._binary_sanity_check(other)
1036
1037 purge_patterns = []
1038
1039
1040 for pat, erangeset in other._patterns.iteritems():
1041
1042 rangeset = self._patterns.get(pat)
1043 if rangeset:
1044
1045 rangeset.difference_update(erangeset, strict)
1046
1047
1048 if len(rangeset) == 0:
1049 purge_patterns.append(pat)
1050 else:
1051
1052 if self._patterns.has_key(pat):
1053 purge_patterns.append(pat)
1054 elif strict:
1055 raise KeyError, pat
1056
1057 for pat in purge_patterns:
1058 del self._patterns[pat]
1059
1061 """
1062 Implement the -= operator. So s -= t returns nodeset s after
1063 removing elements found in t. (Python version 2.5+ required)
1064 """
1065 self._binary_sanity_check(other)
1066 return self.difference_update(other)
1067
1069 """
1070 Remove element elem from the nodeset. Raise KeyError if elem
1071 is not contained in the nodeset.
1072 """
1073 self.difference_update(elem, True)
1074
1076 """
1077 s.symmetric_difference(t) returns the symmetric difference of
1078 two nodesets as a new NodeSet.
1079
1080 (ie. all nodes that are in exactly one of the nodesets.)
1081 """
1082 self_copy = copy.deepcopy(self)
1083 self_copy.symmetric_difference_update(other)
1084 return self_copy
1085
1087 """
1088 Implement the ^ operator. So s ^ t returns a new NodeSet with
1089 nodes that are in exactly one of the nodesets.
1090 """
1091 if not isinstance(other, NodeSet):
1092 return NotImplemented
1093 return self.symmetric_difference(other)
1094
1096 """
1097 s.symmetric_difference_update(t) returns nodeset s keeping all
1098 nodes that are in exactly one of the nodesets.
1099 """
1100 self._binary_sanity_check(other)
1101 purge_patterns = []
1102
1103
1104 for pat, rangeset in self._patterns.iteritems():
1105 brangeset = other._patterns.get(pat)
1106 if brangeset:
1107 rangeset.symmetric_difference_update(brangeset)
1108 else:
1109 if other._patterns.has_key(pat):
1110 purge_patterns.append(pat)
1111
1112
1113 for pat, brangeset in other._patterns.iteritems():
1114 rangeset = self._patterns.get(pat)
1115 if not rangeset and not self._patterns.has_key(pat):
1116 self._add(pat, brangeset)
1117
1118
1119 for pat, rangeset in self._patterns.iteritems():
1120 if rangeset is not None and len(rangeset) == 0:
1121 purge_patterns.append(pat)
1122
1123
1124 for pat in purge_patterns:
1125 del self._patterns[pat]
1126
1128 """
1129 Implement the ^= operator. So s ^= t returns nodeset s after
1130 keeping all nodes that are in exactly one of the nodesets.
1131 (Python version 2.5+ required)
1132 """
1133 self._binary_sanity_check(other)
1134 return self.symmetric_difference_update(other)
1135
1138 """
1139 """
1140 - def _add(self, pat, rangeset):
1141 """
1142 Add groups from a (pat, rangeset) tuple. `pat' may be an existing
1143 pattern and `rangeset' may be None.
1144 """
1145 if pat and pat[0] != '@':
1146 raise ValueError("NodeGroup name must begin with character '@'")
1147 NodeSetBase._add(self, pat, rangeset)
1148
1151 """
1152 Class that is able to transform a source into a NodeSetBase.
1153 """
1154 OP_CODES = { 'update': ',',
1155 'difference_update': '!',
1156 'intersection_update': '&',
1157 'symmetric_difference_update': '^' }
1158
1160 """
1161 Initialize Parsing Engine.
1162 """
1163 self.group_resolver = group_resolver
1164
1165 - def parse(self, nsobj, autostep):
1166 """
1167 Parse provided object if possible and return a NodeSetBase object.
1168 """
1169
1170 if nsobj is None:
1171 return NodeSetBase()
1172
1173
1174 if isinstance(nsobj, NodeSetBase):
1175 return nsobj
1176
1177
1178 if type(nsobj) is str:
1179 try:
1180 return self.parse_string(str(nsobj), autostep)
1181 except NodeUtils.GroupSourceQueryFailed, exc:
1182 raise NodeSetParseError(nsobj, str(exc))
1183
1184 raise TypeError("Unsupported NodeSet input %s" % type(nsobj))
1185
1187 """
1188 Parse provided string and return a NodeSetBase object.
1189 """
1190 ns = NodeSetBase()
1191
1192 for opc, pat, rangeset in self._scan_string(nsstr, autostep):
1193
1194
1195 if self.group_resolver and pat[0] == '@':
1196 ns_group = NodeSetBase()
1197 for nodegroup in NodeGroupBase(pat, rangeset):
1198
1199 ns_string_ext = self.parse_group_string(nodegroup)
1200 if ns_string_ext:
1201
1202 ns_group.update(self.parse(ns_string_ext, autostep))
1203
1204 getattr(ns, opc)(ns_group)
1205 else:
1206 getattr(ns, opc)(NodeSetBase(pat, rangeset))
1207
1208 return ns
1209
1210 - def parse_group(self, group, namespace=None, autostep=None):
1211 """Parse provided single group name (without @ prefix)."""
1212 assert self.group_resolver is not None
1213 nodestr = self.group_resolver.group_nodes(group, namespace)
1214 return self.parse(",".join(nodestr), autostep)
1215
1217 """Parse provided group string and return a string."""
1218 assert nodegroup[0] == '@'
1219 assert self.group_resolver is not None
1220 grpstr = nodegroup[1:]
1221 if grpstr.find(':') < 0:
1222
1223 return ",".join(self.group_resolver.group_nodes(grpstr))
1224 else:
1225
1226 namespace, group = grpstr.split(':', 1)
1227 return ",".join(self.group_resolver.group_nodes(group, namespace))
1228
1230 """Opcode parsing subroutine."""
1231 op_idx = -1
1232 next_op_code = None
1233 for opc, idx in [(k, pat.find(v)) \
1234 for k, v in ParsingEngine.OP_CODES.iteritems()]:
1235 if idx >= 0 and (op_idx < 0 or idx <= op_idx):
1236 next_op_code = opc
1237 op_idx = idx
1238 return op_idx, next_op_code
1239
1241 """
1242 Parsing engine's string scanner method.
1243 """
1244 single_node_re = None
1245 pat = nsstr.strip()
1246
1247 if pat.find('%') >= 0:
1248 pat = pat.replace('%', '%%')
1249 next_op_code = 'update'
1250 while pat is not None:
1251
1252 pat = pat.lstrip()
1253
1254 op_code, next_op_code = next_op_code, None
1255 op_idx = -1
1256 op_idx, next_op_code = self._next_op(pat)
1257 bracket_idx = pat.find('[')
1258
1259
1260
1261 if bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0):
1262
1263
1264
1265
1266 pfx, sfx = pat.split('[', 1)
1267 try:
1268 rg, sfx = sfx.split(']', 1)
1269 except ValueError:
1270 raise NodeSetParseError(pat, "missing bracket")
1271
1272
1273 op_idx, next_op_code = self._next_op(sfx)
1274 if op_idx < 0:
1275 pat = None
1276 else:
1277 sfx, pat = sfx.split(self.OP_CODES[next_op_code], 1)
1278
1279
1280 sfx = sfx.rstrip()
1281
1282
1283 if len(pfx) + len(sfx) == 0:
1284 raise NodeSetParseError(pat, "empty node name")
1285
1286
1287 try:
1288 rset = RangeSet(rg, autostep)
1289 except RangeSetParseError, e:
1290 raise NodeSetParseRangeError(e)
1291
1292 yield op_code, "%s%%s%s" % (pfx, sfx), rset
1293 else:
1294
1295
1296
1297 if op_idx < 0:
1298 node = pat
1299 pat = None
1300 else:
1301 node, pat = pat.split(self.OP_CODES[next_op_code], 1)
1302
1303 node = node.strip()
1304
1305 if len(node) == 0:
1306 raise NodeSetParseError(pat, "empty node name")
1307
1308
1309 if single_node_re is None:
1310 single_node_re = re.compile("(\D*)(\d*)(.*)")
1311
1312 mo = single_node_re.match(node)
1313 if not mo:
1314 raise NodeSetParseError(pat, "parse error")
1315 pfx, idx, sfx = mo.groups()
1316 pfx, sfx = pfx or "", sfx or ""
1317
1318
1319 if len(pfx) + len(sfx) == 0:
1320 raise NodeSetParseError(pat, "empty node name")
1321
1322 if idx:
1323 try:
1324 rset = RangeSet(idx, autostep)
1325 except RangeSetParseError, e:
1326 raise NodeSetParseRangeError(e)
1327 p = "%s%%s%s" % (pfx, sfx)
1328 yield op_code, p, rset
1329 else:
1330
1331 yield op_code, pfx, None
1332
1333
1334
1335
1336 NOGROUP_RESOLVER=-1
1337
1338
1339 -class NodeSet(NodeSetBase):
1340 """
1341 Iterable class of nodes with node ranges support.
1342
1343 NodeSet creation examples:
1344 >>> nodeset = NodeSet() # empty NodeSet
1345 >>> nodeset = NodeSet("cluster3") # contains only cluster3
1346 >>> nodeset = NodeSet("cluster[5,10-42]")
1347 >>> nodeset = NodeSet("cluster[0-10/2]")
1348 >>> nodeset = NodeSet("cluster[0-10/2],othername[7-9,120-300]")
1349
1350 NodeSet provides methods like update(), intersection_update() or
1351 difference_update() methods, which conform to the Python Set API.
1352 However, unlike RangeSet or standard Set, NodeSet is somewhat not
1353 so strict for convenience, and understands NodeSet instance or
1354 NodeSet string as argument. Also, there is no strict definition of
1355 one element, for example, it IS allowed to do:
1356 >>> nodeset.remove("blue[36-40]").
1357 """
1358 - def __init__(self, nodes=None, autostep=None, resolver=None):
1359 """
1360 Initialize a NodeSet.
1361 The `nodes' argument may be a valid nodeset string or a NodeSet
1362 object. If no nodes are specified, an empty NodeSet is created.
1363 """
1364 NodeSetBase.__init__(self)
1365
1366 self._autostep = autostep
1367
1368
1369 self._resolver = None
1370 if resolver != NOGROUP_RESOLVER:
1371 self._resolver = resolver or STD_GROUP_RESOLVER
1372
1373
1374 self._parser = ParsingEngine(self._resolver)
1375
1376 self.update(nodes)
1377
1378 @classmethod
1379 - def fromlist(cls, nodelist, autostep=None, resolver=None):
1380 """
1381 Class method that returns a new NodeSet with nodes from
1382 provided list.
1383 """
1384 inst = NodeSet(autostep=autostep, resolver=resolver)
1385 for node in nodelist:
1386 inst.update(node)
1387 return inst
1388
1389 @classmethod
1390 - def fromall(cls, groupsource=None, autostep=None, resolver=None):
1423
1425 """Called when pickling: remove references to group resolver."""
1426 odict = self.__dict__.copy()
1427 del odict['_resolver']
1428 del odict['_parser']
1429 return odict
1430
1432 """Called when unpickling: restore parser using non group
1433 resolver."""
1434 self.__dict__.update(dict)
1435 self._resolver = None
1436 self._parser = ParsingEngine(None)
1437
1439 """Find groups of node by namespace."""
1440 if allgroups:
1441
1442 for grp, ns in allgroups.iteritems():
1443 if node in ns:
1444 yield grp
1445 else:
1446
1447 for group in self._resolver.node_groups(node, namespace):
1448 yield group
1449
1450 - def regroup(self, groupsource=None, autostep=None, overlap=False,
1451 noprefix=False):
1452 """
1453 Regroup nodeset using groups.
1454 """
1455 groups = {}
1456 rest = NodeSet(self, resolver=NOGROUP_RESOLVER)
1457
1458 try:
1459
1460 allgrpns = NodeSet.fromlist(self._resolver.grouplist(groupsource),
1461 resolver=NOGROUP_RESOLVER)
1462 except NodeUtils.GroupSourceException:
1463
1464
1465 allgrpns = None
1466
1467 allgroups = {}
1468
1469
1470
1471
1472 if self._resolver.has_node_groups(groupsource) and \
1473 (not allgrpns or len(allgrpns) >= len(self)):
1474
1475 pass
1476 else:
1477 if not allgrpns:
1478 return str(rest)
1479 try:
1480
1481 for grp in allgrpns:
1482 nodelist = self._resolver.group_nodes(grp, groupsource)
1483 allgroups[grp] = NodeSet(",".join(nodelist))
1484 except NodeUtils.GroupSourceQueryFailed, exc:
1485
1486 raise NodeSetExternalError("Unable to map a group " \
1487 "previously listed\n\tFailed command: %s" % exc)
1488
1489
1490 for node in self._iterbase():
1491 for grp in self._find_groups(node, groupsource, allgroups):
1492 if grp not in groups:
1493 ns = self._parser.parse_group(grp, groupsource, autostep)
1494 groups[grp] = (0, ns)
1495 i, m = groups[grp]
1496 groups[grp] = (i + 1, m)
1497
1498
1499 fulls = []
1500 for k, (i, m) in groups.iteritems():
1501 assert i <= len(m)
1502 if i == len(m):
1503 fulls.append((i, k))
1504
1505 regrouped = NodeSet(resolver=NOGROUP_RESOLVER)
1506
1507 bigalpha = lambda x,y: cmp(y[0],x[0]) or cmp(x[1],y[1])
1508
1509
1510 for num, grp in sorted(fulls, cmp=bigalpha):
1511 if not overlap and groups[grp][1] not in rest:
1512 continue
1513 if groupsource and not noprefix:
1514 regrouped.update("@%s:%s" % (groupsource, grp))
1515 else:
1516 regrouped.update("@" + grp)
1517 rest.difference_update(groups[grp][1])
1518 if not rest:
1519 return str(regrouped)
1520
1521 if regrouped:
1522 return "%s,%s" % (regrouped, rest)
1523
1524 return str(rest)
1525
1527 """
1528 Report whether another nodeset contains this nodeset.
1529 """
1530 ns = self._parser.parse(other, self._autostep)
1531 return NodeSetBase.issuperset(ns, self)
1532
1534 """
1535 Report whether this nodeset contains another nodeset.
1536 """
1537 ns = self._parser.parse(other, self._autostep)
1538 return NodeSetBase.issuperset(self, ns)
1539
1541 """
1542 Return the slice from index i to index j-1. For convenience
1543 only, not optimized as of version 1.0.
1544 """
1545 return NodeSet.fromlist(list(self)[i:j])
1546
1548 """
1549 Split the nodeset into nbr sub-nodeset. Each sub-nodeset will have the
1550 same number of element more or less 1. Current nodeset remains
1551 unmodified.
1552
1553 >>> NodeSet("foo[1-5]").split(3)
1554 NodeSet("foo[1-2]")
1555 NodeSet("foo[3-4]")
1556 NodeSet("foo5")
1557 """
1558
1559 assert(nbr > 0)
1560
1561
1562 slice_size = len(self) / nbr
1563 left = len(self) % nbr
1564
1565 begin = 0
1566 for i in range(0, nbr):
1567 length = slice_size + int(i < left)
1568 yield self[begin:begin + length]
1569 begin += length
1570
1572 """
1573 s.update(t) returns nodeset s with elements added from t.
1574 """
1575 ns = self._parser.parse(other, self._autostep)
1576 NodeSetBase.update(self, ns)
1577
1579 """
1580 s.intersection_update(t) returns nodeset s keeping only
1581 elements also found in t.
1582 """
1583 ns = self._parser.parse(other, self._autostep)
1584 NodeSetBase.intersection_update(self, ns)
1585
1587 """
1588 s.difference_update(t) returns nodeset s after removing
1589 elements found in t. If strict is True, raise KeyError
1590 if an element cannot be removed.
1591 """
1592 ns = self._parser.parse(other, self._autostep)
1593 NodeSetBase.difference_update(self, ns, strict)
1594
1596 """
1597 s.symmetric_difference_update(t) returns nodeset s keeping all
1598 nodes that are in exactly one of the nodesets.
1599 """
1600 ns = self._parser.parse(other, self._autostep)
1601 NodeSetBase.symmetric_difference_update(self, ns)
1602
1605 """
1606 Commodity function that expands a pdsh-like pattern into a list of
1607 nodes.
1608 """
1609 return list(NodeSet(pat))
1610
1612 """
1613 Commodity function that clean dups and fold provided pattern with
1614 ranges and "/step" support.
1615 """
1616 return str(NodeSet(pat))
1617
1619 """
1620 Commodity function that retrieves the list of groups for a specified
1621 group namespace (or use default namespace).
1622 """
1623 return STD_GROUP_RESOLVER.grouplist(namespace)
1624
1625
1626
1627
1628 -def _test():
1629 import doctest
1630 doctest.testmod()
1631
1632 if __name__ == '__main__':
1633 _test()
1634