File: Nodeset.py

package info (click to toggle)
clustershell 1.9.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,228 kB
  • sloc: python: 20,978; makefile: 149
file content (365 lines) | stat: -rwxr-xr-x 14,268 bytes parent folder | download
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#
# Copyright (C) 2008-2016 CEA/DAM
# Copyright (C) 2015-2018 Stephane Thiell <sthiell@stanford.edu>
#
# This file is part of ClusterShell.
#
# ClusterShell is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# ClusterShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with ClusterShell; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""
compute advanced nodeset operations

The nodeset command is an utility command provided with the
ClusterShell library which implements some features of the NodeSet
and RangeSet classes.
"""

from __future__ import print_function

import logging
import math
import random
import sys

from ClusterShell.CLI.Error import GENERIC_ERRORS, handle_generic_error
from ClusterShell.CLI.OptionParser import OptionParser

from ClusterShell.NodeSet import NodeSet, RangeSet, std_group_resolver
from ClusterShell.NodeSet import grouplist, set_std_group_resolver_config
from ClusterShell.NodeUtils import GroupSourceNoUpcall


def process_stdin(xsetop, xsetcls, autostep):
    """Process standard input and operate on xset."""
    # Build temporary set (stdin accumulator)
    tmpset = xsetcls(autostep=autostep)
    for line in sys.stdin:  # read lines of text stream (not bytes)
        # Support multi-lines and multi-nodesets per line
        line = line[0:line.find('#')].strip()
        for elem in line.split():
            # Do explicit object creation for RangeSet
            tmpset.update(xsetcls(elem, autostep=autostep))
    # Perform operation on xset
    if tmpset:
        xsetop(tmpset)

def compute_nodeset(xset, args, autostep):
    """Apply operations and operands from args on xset, an initial
    RangeSet or NodeSet."""
    class_set = xset.__class__
    # Process operations from command arguments.
    # The special argument string "-" indicates to read stdin.
    # We also take care of multiline shell arguments (#394).
    while args:
        arg = args.pop(0)
        if arg in ("-i", "--intersection"):
            val = args.pop(0)
            if val == '-':
                process_stdin(xset.intersection_update, class_set, autostep)
            else:
                xset.intersection_update(class_set.fromlist(val.splitlines(),
                                                            autostep=autostep))
        elif arg in ("-x", "--exclude"):
            val = args.pop(0)
            if val == '-':
                process_stdin(xset.difference_update, class_set, autostep)
            else:
                xset.difference_update(class_set.fromlist(val.splitlines(),
                                                          autostep=autostep))
        elif arg in ("-X", "--xor"):
            val = args.pop(0)
            if val == '-':
                process_stdin(xset.symmetric_difference_update, class_set,
                              autostep)
            else:
                xset.symmetric_difference_update(
                    class_set.fromlist(val.splitlines(), autostep=autostep))
        elif arg == '-':
            process_stdin(xset.update, xset.__class__, autostep)
        else:
            xset.update(class_set.fromlist(arg.splitlines(), autostep=autostep))

    return xset

def print_source_groups(source, level, xset, opts):
    """
    Print groups from a source, a level of verbosity and an optional
    nodeset acting as a filter.
    """
    # list groups of some specified nodes?
    if opts.all or xset or opts.and_nodes or opts.sub_nodes or opts.xor_nodes:
        # When some node sets are provided as argument, the list command
        # retrieves node groups these nodes belong to, thanks to the
        # groups() method.
        # Note: stdin support is enabled when '-' is found.
        groups = xset.groups(source, opts.groupbase)
        # sort by group name
        for group, (gnodes, inodes) in sorted(groups.items()):
            if level == 1:
                print(group)
            elif level == 2:
                print("%s %s" % (group, inodes))
            else:
                print("%s %s %d/%d" % (group, inodes, len(inodes),
                                       len(gnodes)))
    else:
        # "raw" group list when no argument at all
        for group in grouplist(source):
            if source and not opts.groupbase:
                nsgroup = "@%s:%s" % (source, group)
            else:
                nsgroup = "@%s" % group
            if level == 1:
                print(nsgroup)
            else:
                nodes = NodeSet(nsgroup)
                if level == 2:
                    print("%s %s" % (nsgroup, nodes))
                else:
                    print("%s %s %d" % (nsgroup, nodes, len(nodes)))

def command_list(options, xset, group_resolver):
    """List command handler (-l/-ll/-lll/-L/-LL/-LLL)."""
    list_level = options.list + options.listall + options.completion
    if options.listall:
        # useful: sources[0] is always the default or selected source
        sources = group_resolver.sources()
        # do not print the name of the current group source (either the default
        # or the one specified by -s)
        if sources:
            sources[0] = None
    else:
        sources = [options.groupsource]

    for source in sources:
        try:
            print_source_groups(source, list_level, xset, options)
        except GroupSourceNoUpcall as exc:
            if not options.listall:
                raise
            # missing list upcall is not fatal with -L
            msgfmt = "Warning: No %s upcall defined for group source %s"
            print(msgfmt % (exc, source), file=sys.stderr)

def nodeset():
    """script subroutine"""
    class_set = NodeSet
    usage = "%prog [COMMAND] [OPTIONS] [ns1 [-ixX] ns2|...]"

    parser = OptionParser(usage)
    parser.install_groupsconf_option()
    parser.install_nodeset_commands()
    parser.install_nodeset_operations()
    parser.install_nodeset_options()
    (options, args) = parser.parse_args()

    set_std_group_resolver_config(options.groupsconf)
    group_resolver = std_group_resolver()

    if options.debug:
        logging.basicConfig(level=logging.DEBUG)

    # Check for command presence
    cmdcount = int(options.count) + int(options.expand) + \
               int(options.fold) + int(bool(options.list)) + \
               int(bool(options.listall)) + int(options.regroup) + \
               int(options.groupsources) + int(options.completion)
    if not cmdcount:
        parser.error("No command specified.")
    elif cmdcount > 1:
        parser.error("Multiple commands not allowed.")

    if options.rangeset:
        class_set = RangeSet

    if options.all or options.regroup:
        if class_set != NodeSet:
            parser.error("-a/-r only supported in NodeSet mode")

    if options.maxsplit is not None and options.contiguous:
        parser.error("incompatible splitting options (split, contiguous)")

    if options.maxsplit is None:
        options.maxsplit = 1

    if options.axis and (not options.fold or options.rangeset):
        parser.error("--axis option is only supported when folding nodeset")

    if options.groupsource and not options.quiet and class_set == RangeSet:
        print("WARNING: option group source \"%s\" ignored"
              % options.groupsource, file=sys.stderr)

    # We want -s <groupsource> to act as a substitution of default groupsource
    # (ie. it's not necessary to prefix group names by this group source).
    if options.groupsource:
        group_resolver.default_source_name = options.groupsource

    # The groupsources command simply lists group sources.
    if options.groupsources:
        if options.quiet:
            dispdefault = ""    # don't show (default) if quiet is set
        else:
            dispdefault = " (default)"
        for src in group_resolver.sources():
            print("%s%s" % (src, dispdefault))
            dispdefault = ""
        return

    autostep = options.autostep

    # Do not use autostep for computation when a percentage or the special
    # value 'auto' is specified. Real autostep value is set post-process.
    if isinstance(autostep, float) or autostep == 'auto':
        autostep = None

    # Instantiate RangeSet or NodeSet object
    xset = class_set(autostep=autostep)

    if options.all:
        # Include all nodes from external node groups support.
        xset.update(NodeSet.fromall()) # uses default_source when set

    if not args and not options.all and not (options.list or
                                             options.listall or
                                             options.completion):
        # No need to specify '-' to read stdin in these cases
        process_stdin(xset.update, xset.__class__, autostep)

    if not xset and (options.and_nodes or options.sub_nodes or
                     options.xor_nodes) and not options.quiet:
        print('WARNING: empty left operand for set operation', file=sys.stderr)

    # Apply first operations (before first non-option)
    for nodes in options.and_nodes:
        if nodes == '-':
            process_stdin(xset.intersection_update, xset.__class__, autostep)
        else:
            xset.intersection_update(class_set(nodes, autostep=autostep))
    for nodes in options.sub_nodes:
        if nodes == '-':
            process_stdin(xset.difference_update, xset.__class__, autostep)
        else:
            xset.difference_update(class_set(nodes, autostep=autostep))
    for nodes in options.xor_nodes:
        if nodes == '-':
            process_stdin(xset.symmetric_difference_update, xset.__class__,
                          autostep)
        else:
            xset.symmetric_difference_update(class_set(nodes,
                                                       autostep=autostep))

    # Finish xset computing from args
    compute_nodeset(xset, args, autostep)

    # The list command has a special handling
    if options.list > 0 or options.listall > 0:
        return command_list(options, xset, group_resolver)
    elif options.completion:  # --completion is used for bash completion
        # list group source prefixes unless source is already specified
        if not options.groupsource:
            for src in group_resolver.sources():
                print("@%s:" % src)
        # list groups in group source (similar to list)
        command_list(options, None, group_resolver)
        # then list nodes from the groups passed as argument, if any
        if not xset:
            return
        options.expand = True

    # Interpret special characters (may raise SyntaxError)
    separator = eval('\'\'\'%s\'\'\'' % options.separator,
                     {"__builtins__":None}, {})

    if options.slice_rangeset:
        _xset = class_set()
        for sli in RangeSet(options.slice_rangeset).slices():
            _xset.update(xset[sli])
        xset = _xset

    if options.autostep == 'auto':
        # Simple implementation of --autostep=auto
        # if we have at least 3 nodes, all index should be foldable as a-b/n
        xset.autostep = max(3, len(xset))
    elif isinstance(options.autostep, float):
        # at least % of nodes should be foldable as a-b/n
        autofactor = float(options.autostep)
        xset.autostep = int(math.ceil(float(len(xset)) * autofactor))

    # user-specified nD-nodeset fold axis
    if options.axis:
        if not options.axis.startswith('-'):
            # axis are 1-indexed in nodeset CLI (0 ignored)
            xset.fold_axis = tuple(x-1 for x in \
                                   RangeSet(options.axis).intiter() if x > 0)
        else:
            # negative axis index (only single number supported)
            xset.fold_axis = [int(options.axis)]

    if options.pick and options.pick < len(xset):
        # convert to string for sample as nsiter() is slower for big
        # nodesets; and we assume options.pick will remain small-ish
        keep = random.sample(list(xset), options.pick)
        # explicit class_set creation and str() conversion for RangeSet
        keep = class_set(','.join([str(x) for x in keep]))
        xset.intersection_update(keep)

    fmt = options.output_format # default to '%s'

    # Display result according to command choice
    if options.expand:
        xsubres = lambda x: separator.join((fmt % s for s in x.striter()))
    elif options.fold:
        # Special case when folding using NodeSet and format is set (#277)
        if class_set is NodeSet and fmt != '%s':
            # Create a new set after format has been applied to each node
            xset = class_set._fromlist1((fmt % xnodestr for xnodestr in xset),
                                        autostep=xset.autostep)
            xsubres = lambda x: x
        else:
            xsubres = lambda x: fmt % x
    elif options.regroup:
        xsubres = lambda x: fmt % x.regroup(options.groupsource,
                                            noprefix=options.groupbase)
    else:
        xsubres = lambda x: fmt % len(x)

    if not xset or options.maxsplit <= 1 and not options.contiguous:
        print(xsubres(xset))
    else:
        if options.contiguous:
            xiterator = xset.contiguous()
        else:
            xiterator = xset.split(options.maxsplit)
        for xsubset in xiterator:
            print(xsubres(xsubset))

def main():
    """main script function"""
    try:
        nodeset()
    except (AssertionError, IndexError, ValueError) as ex:
        print("ERROR: %s" % ex, file=sys.stderr)
        sys.exit(1)
    except SyntaxError:
        print("ERROR: invalid separator", file=sys.stderr)
        sys.exit(1)
    except GENERIC_ERRORS as ex:
        sys.exit(handle_generic_error(ex))

    sys.exit(0)


if __name__ == '__main__':
    main()