source: trunk/examples/sara_nodes.py.in @ 363

Last change on this file since 363 was 363, checked in by martijk, 6 years ago

use system python

  • Property svn:executable set to *
File size: 24.8 KB
Line 
1#!@PYTHON@
2#
3# Author: Dennis Stam
4# Date  : 6th of September 2012
5#
6# Tested with Python 2.5, 2.6, 2.7 (should work for 3.0, 3.1, 3.2)
7#   sara_nodes uses the module argparse
8
9## The documenation, is shown when you type --help
10from __future__ import print_function
11
12HELP_DESCRIPTION = '''This program is a great example what you can achieve with the pbs_python wrapper. You can use sara_nodes to change the state of a machine to offline with a reason. Several information is stored in the nodes note attribute. Information such as; date added, date last change, the username, ticket number (handy when you wan't to reference to an issue in your tracking system) and the message.'''
13HELP_EPILOG = '''The format argument uses the Python string formatting. Fields that can be used are; nodename, state, date_add, date_edit, username, ticket and note. For example sara_nodes -f '%(nodename)s;%(state)s' '''
14
15## This variable is used to sort by basename
16SPLIT_SORT = r'r\d+n\d+'
17## This RE pattern is used for the hostrange
18HOSTRANGE = r'\[([0-9az\-,]+)\]'
19## Which states are allowed to show in the print_overview
20ALLOWED_STATES = set(['down', 'offline', 'unknown'])
21
22import pbs
23import PBSQuery
24import re
25import sys
26import time
27import getpass
28import types
29
30## Use the cli arguments to change the values
31ARGS_QUIET      = False
32ARGS_VERBOSE    = False
33ARGS_DRYRUN     = False
34
35####
36## Rewriting the print function, so it will work with all versions of Python
37def _print(*args, **kwargs):
38    '''A wrapper function to make the functionality for the print function the same for Python2.4 and higher'''
39
40    ## First try if we are running in Python3 and higher
41    try:
42        Print = eval('print')
43        Print(*args, **kwargs)
44    except SyntaxError:
45        ## Then Python2.6 and Python2.7
46        try:
47            D = dict()
48            exec('from __future__ import print_function\np=print', D)
49            D['p'](*args, **kwargs)
50            del D
51        ## Finally Python2.5 or lower
52        except SyntaxError:
53            del D
54            fout    = kwargs.get('file', sys.stdout)
55            write   = fout.write
56            if args:
57                write(str(args[0]))
58                sep = kwargs.get('sep', ' ')
59                for arg in args[1:]:
60                    write(sep)
61                    write(str(a))
62                write(kwargs.get('end', '\n'))
63
64## Import argparse here, as I need the _print function
65try:
66    import argparse
67except ImportError:
68    _print('Cannot find argparse module', file=sys.stderr)
69    sys.exit(1)
70
71####
72## BEGIN functions for hostrange parsing
73def l_range(start, end):
74    '''The equivalent for the range function, but then with letters, uses the ord function'''
75    start = ord(start)
76    end   = ord(end)
77    rlist = list()
78
79    ## A ord number must be between 96 (a == 97) and 122 (z == 122)
80    if start < 96 or start > 122 and end < 96 or end > 122:
81        raise Exception('You can only use letters a to z')
82    ## If start is greater then end, then the range is invalid
83    elif start > end:
84        raise Exception('The letters must be in alphabetical order')
85    ## Just revert the ord number to the char
86    for letter in range(start, end + 1):
87        rlist.append(chr(letter))
88    return rlist
89
90def return_range(string):
91    '''This function will return the possible values for the given ranges'''
92
93    ## First check if the first char is valid
94    if string.startswith(',') or string.startswith('-'):
95        raise Exception('Given pattern is invalid, you can\'t use , and - at the beginning')
96
97    numbers_chars        = list()
98    equal_width_length  = 0
99
100    ## First splitup the sections (divided by ,)
101    for section in string.split(','):
102        ## Within a section you can have a range, max two values
103        chars = section.split('-')
104        if len(chars) == 2:
105            ## When range is a digit, simply use the range function
106            if chars[0].isdigit() and chars[1].isdigit():
107                ## Owke, check for equal_width_length
108                if chars[0][0] == '0' or chars[1][0] == '0':
109                    if len(chars[0]) >= len(chars[1]):
110                        equal_width_length = len(chars[0])
111                    else:
112                        equal_width_length = len(chars[1])
113                ## Don't forget the +1
114                numbers_chars += range(int(chars[0]), int(chars[1])+1)
115            ## If one of the two is a digit, raise an exception
116            elif chars[0].isdigit() or chars[1].isdigit():
117                raise Exception('I can\'t combine integers with letters, change your range please')
118            ## Else use the l_range
119            else:
120                numbers_chars += l_range(chars[0], chars[1])
121        else:
122            ## If the value of the section is a integer value, check if it has a 0
123            if section.isdigit() and section[0] == '0':
124                if len(section) > equal_width_length:
125                    equal_width_length = len(section)
126            numbers_chars.append(section)
127
128        ## if the equal_width length is greater then 0, rebuild the list
129        ## 01, 02, 03, ... 10
130        if equal_width_length > 0:
131            tmp_list = list()
132            for number_char in numbers_chars:
133                if type(number_char) is types.IntType or number_char.isdigit():
134                    tmp_list.append('%0*d' % ( equal_width_length, int(number_char)))
135                else:
136                    tmp_list.append(number_char)
137            numbers_chars = tmp_list
138
139    return numbers_chars
140
141def product(*args, **kwargs):
142    '''Taken from the python docs, does the same as itertools.product,
143    but this also works for py2.5'''
144    pools = map(tuple, args) * kwargs.get('repeat', 1)
145    result = [[]]
146    for pool in pools:
147        result = [x+[y] for x in result for y in pool]
148    for prod in result:
149        yield tuple(prod)
150
151def parse_args(args):
152    rlist = list()
153    for arg in args:
154        parts = re.findall(HOSTRANGE, arg)
155        if parts:
156            ## create a formatter string, sub the matched patternn with %s
157            string_format = re.sub(HOSTRANGE, '%s', arg)
158            ranges = list()
159
160            ## detect the ranges in the parts
161            for part in parts:
162                ranges.append(return_range(part))
163           
164            ## produce the hostnames
165            for combination in product(*ranges):
166                rlist.append(string_format % combination)
167        else:
168            rlist.append(arg)
169    return rlist
170
171def get_nodes(jobs):
172
173    p = PBSQuery.PBSQuery()
174    nodes = list()
175
176    for job in jobs:
177        exec_hosts = p.getjob(job, ['exec_host'])
178        if not exec_hosts or 'exec_host' not in exec_hosts:
179            continue
180        for exec_host in exec_hosts['exec_host'][0].split('+'):
181            hostname = exec_host.split('/')[0]
182            if hostname not in nodes:
183                nodes.append(hostname)
184    return nodes
185
186## END functions for hostrange parsing
187####
188
189####
190## BEGIN functions for printing
191def _generate_index(string):
192    '''Is used to generate a index, this way we can also sort nummeric values in a string'''
193    return [ int(y) if y.isdigit() else y for y in re.split(r'(\d+)', string) ]
194
195def print_get_nodes(hosts=None):
196    '''This function retrieves the information from your batch environment'''
197    if ARGS_VERBOSE:
198        _print('func:print_get_nodes input:%s' % str(hosts), file=sys.stderr)
199
200    ## there are 2 possible filters, by hostname, or by state
201    pbsq         = PBSQuery.PBSQuery()
202    split_1     = dict()
203    split_2     = dict()
204
205    if ARGS_VERBOSE:
206        _print('func:print_get_nodes fetching node information', file=sys.stderr)
207    ## We ask from the batch all nodes, and with the properties state and note
208    for host, properties in pbsq.getnodes(['state', 'note']).items():
209        do_host = None
210        ## Check if the current host matches our criterium (given with the arguments
211        ## or has the allowed state)
212        if hosts and host in hosts:
213            do_host = host
214        elif not hosts:
215            ## Do a intersection on both set's, if there is a match, then the host is allowed
216            if bool(ALLOWED_STATES.intersection(set(properties.state))):
217                do_host = host
218
219        ## when we have a do_host (means matches our criterium) then sort
220        ## them by basename
221        if do_host:
222            if SPLIT_SORT and re.findall(SPLIT_SORT, do_host):
223                split_1[host] = properties
224            else:
225                split_2[host] = properties
226   
227    if ARGS_VERBOSE:
228        _print('func:print_get_nodes returning values', file=sys.stderr)
229    return split_1, split_2
230
231def print_process_dict(dictin):
232    '''This function processes the data from the batch system and make it for all hosts the same layout'''
233    if ARGS_VERBOSE:
234        _print('func:print_process_dict input:%s' % str(dictin), file=sys.stderr)
235
236    line_print = list()
237    if ARGS_VERBOSE:
238        _print('func:print_process_dict processing data', file=sys.stderr)
239
240    ## Generate a list containing a dictionary, so we can use the stringformatting functionality
241    ## of Python, fieldnames are: nodename, date_edit, date_add, username, ticket, note
242
243    ## Replaced real_sort with sorted, this means from 50 lines of code to 3
244    for host in sorted(dictin.keys(), key=_generate_index):
245        add_dict = dict()
246
247        add_dict['nodename'] = host
248        ## Glue the state list with a ,
249        add_dict['state'] = ", ".join(dictin[host]['state'] if dictin[host].has_key('state') else [])
250
251        ## Check if the given host has a note
252        note = dictin[host]['note'] if dictin[host].has_key('note') else []
253        if len(note) > 3:
254            add_dict['date_add']    = note[0]
255            add_dict['date_edit']   = note[1]
256            add_dict['username']    = note[2]
257
258            if note[3].isdigit():
259                add_dict['ticket']      = note[3]
260                add_dict['note']        = ",".join(note[4:])
261            else:
262                add_dict['ticket']      = ""
263                add_dict['note']        = ",".join(note[3:])
264
265            ## Create an extra date field, combined for date_edit and date_add
266            if add_dict['date_add'] and add_dict['date_edit']:
267                add_dict['date'] = '%s, %s' % (add_dict['date_add'], add_dict['date_edit'])
268            elif add_dict['date_add']:
269                add_dict['date'] = add_dict['date_add']
270            else:
271                add_dict['date'] = None
272
273        else:
274            ## If there is no note, just set the variables with a empty string
275            add_dict['date'] = add_dict['date_add'] = add_dict['date_edit'] = add_dict['username'] = add_dict['ticket'] = add_dict['note'] = ''
276
277        line_print.append(add_dict)
278
279    if ARGS_VERBOSE:
280        _print('func:print_process_dict returning values', file=sys.stderr)
281    return line_print
282
283def print_create_list(values):
284    tmp_list = list()
285    for pair in values:
286        tmp_list.append('%-*s' % tuple(pair))
287    return tmp_list
288
289def print_overview_normal(hosts=None):
290    '''Print the default overview'''
291    if ARGS_VERBOSE:
292        _print('func:print_overview_normal input:%s' % str(hosts), file=sys.stderr)
293
294    ## Determine some default values for the column width
295    w_nodename = 8
296    w_state = 5
297    w_date = w_username = w_ticket = w_note = w_date_add = w_date_edit = 0
298
299    ## Get the data, make it one list, the rest first then the matched
300    matched, rest = print_get_nodes(hosts)
301    print_list = print_process_dict(rest)
302    print_list.extend(print_process_dict(matched))
303
304    ## Detect the max width for the columns
305    for line in print_list:
306        if line['nodename'] and len(line['nodename']) > w_nodename:
307            w_nodename = len(line['nodename'])
308        if line['state'] and len(line['state']) > w_state:
309            w_state = len(line['state'])
310        if line['date'] and len(line['date']) > w_date:
311            w_date = len(line['date'])
312        if line['date_add'] and len(line['date_add']) > w_date_add:
313            w_date_add = len(line['date_add'])
314        if line['date_edit'] and len(line['date_edit']) > w_date_edit:
315            w_date_edit = len(line['date_edit'])
316        if line['username'] and len(line['username']) > w_username:
317            w_username = len(line['username'])
318        if line['ticket'] and len(line['ticket']) > w_ticket:
319            w_ticket = len(line['ticket'].strip())
320        if line['note'] and len(line['note']) > w_note:
321            w_note = len(line['note'])
322
323    ## The length of the full note
324    w_notefull  = w_date + w_username + w_ticket + w_note
325
326    if not ARGS_QUIET:
327        show_fields = [
328            [w_nodename, 'Nodename'],
329            [w_state, 'State'],
330        ]
331        if w_date > 0:
332            show_fields.append([w_date_add,'Added'])
333            show_fields.append([w_date_edit,'Modified'])
334            show_fields.append([w_username,'User'])
335            if w_ticket > 0:
336                if w_ticket < 6:
337                    w_ticket = 6
338                show_fields.append([w_ticket,'Ticket'])
339            show_fields.append([w_note,'Note'])
340
341        _print(' %s' % ' | '.join(print_create_list(show_fields)))
342        _print('+'.join([ '-' * (show_field[0]+2) for show_field in show_fields ]))
343
344    ## Show the information to the user
345    for line in print_list:
346        show_line_fields = [
347            [w_nodename, line['nodename']],
348            [w_state, line['state']],
349        ]
350        if w_date > 0:
351            show_line_fields.append([w_date_add,line['date_add']])
352            show_line_fields.append([w_date_edit,line['date_edit']])
353            show_line_fields.append([w_username,line['username']])
354            if w_ticket > 0 and line['ticket'].strip():
355                show_line_fields.append([w_ticket,'#' + line['ticket']])
356            else:
357                show_line_fields.append([w_ticket,''])
358            show_line_fields.append([w_note,line['note']])
359
360        _print(' %s' % ' | '.join(print_create_list(show_line_fields)))
361
362def print_overview_format(hosts=None, format=None):
363    '''Print the information in a certain format, when you want to use it in a
364    different program'''
365
366    matched, rest = print_get_nodes(hosts)
367    print_list = print_process_dict(rest)
368    print_list.extend(print_process_dict(matched))
369
370    for line in print_list:
371        _print(format % line)
372## END functions for printing
373####
374
375class SaraNodes(object):
376    '''This class is used to communicate with the batch server'''
377
378    ticket      = None
379
380    def _get_current_notes(self, nodes):
381        '''A function to retrieve the current message'''
382        if ARGS_VERBOSE:
383            _print('class:SaraNodes func:_get_current_notes input:%s' % str(nodes), file=sys.stderr)
384
385        pbsq = PBSQuery.PBSQuery()
386        rdict = dict()
387
388        ## We are only intereseted in the note
389        for node, properties in pbsq.getnodes(['note']).items():
390            if node in nodes and properties.has_key('note'):
391                rdict[node] = properties['note']
392        return rdict
393
394    def _get_curdate(self):
395        '''Returns the current time'''
396        if ARGS_VERBOSE:
397            _print('class:SaraNodes func:_get_curdate', file=sys.stderr)
398        return time.strftime('%d-%m %H:%M', time.localtime())
399
400    def _get_uid(self, prev_uid=None):
401        '''Get the username'''
402        if ARGS_VERBOSE:
403            _print('class:SaraNodes func:_get_uid input:%s' % prev_uid, file=sys.stderr)
404        cur_uid = getpass.getuser()
405        if prev_uid and cur_uid == 'root':
406            return prev_uid
407        return cur_uid
408
409    def _get_ticket(self, prev_ticket=None):       
410        '''Check if we already have a ticket number'''
411        if ARGS_VERBOSE:
412            _print('class:SaraNodes func:_get_ticket input:%s' % prev_ticket, file=sys.stderr)
413        cur_ticket = '%s' % self.ticket
414        if prev_ticket and cur_ticket == prev_ticket:
415            return prev_ticket
416        elif self.ticket:
417            return cur_ticket
418        elif self.ticket in ['c','clear','N',]:
419            return ''
420        elif prev_ticket:
421            return prev_ticket
422        return ''
423
424    def _generate_note(self, nodes=None, note=None, append=True):
425        '''Generates the node in a specific format'''
426        if ARGS_VERBOSE:
427            _print('class:SaraNodes func:_generate_note input:%s,%s,%s' % (str(nodes), note, str(append)), file=sys.stderr)
428
429        ## First step, is to get the current info of a host
430        cur_data = self._get_current_notes(nodes)
431        rdict = dict()
432
433        for node in nodes:
434            date_add = date_edit = username = ticket = nnote = None
435            if node in cur_data.keys():
436                date_add    = cur_data[node][0]
437                date_edit   = self._get_curdate()
438                username    = self._get_uid(cur_data[node][2])
439                ticket      = self._get_ticket(cur_data[node][3])
440                nnote       = ",".join(cur_data[node][4:])
441            else:
442                date_add = date_edit = self._get_curdate()
443                username = self._get_uid()
444                ticket   = self._get_ticket()
445                nnote    = None
446
447            if nnote and append and note:
448                nnote = '%s, %s' % (nnote, note)
449            elif note:
450                nnote = note
451
452            rdict[node] = '%s,%s,%s,%s,%s' % (date_add, date_edit, username, ticket, nnote)
453        return rdict
454
455    def do_offline(self, nodes, note):
456        '''Change the state of node(s) to offline with a specific note'''
457
458        if ARGS_VERBOSE:
459            _print('class:SaraNodes func:do_offline input:%s,%s' % (str(nodes), note), file=sys.stderr)
460        attributes          = pbs.new_attropl(2)
461        attributes[0].name  = pbs.ATTR_NODE_state
462        attributes[0].value = 'offline'
463        attributes[0].op    = pbs.SET
464        attributes[1].name  = pbs.ATTR_NODE_note
465        attributes[1].op    = pbs.SET
466
467        batch_list = list()
468
469        ## again a loop, now create the attrib dict list
470        for node, note in self._generate_note(nodes, note).items():
471            attributes[1].value = note
472            batch_list.append(tuple([node, attributes]))
473
474        self._process(batch_list)
475
476    def do_clear(self, nodes):
477        '''Clear the state on a node(s) to down, also clear the note'''
478
479        if ARGS_VERBOSE:
480            _print('class:SaraNodes func:do_clear input:%s' % str(nodes), file=sys.stderr)
481        attributes          = pbs.new_attropl(2)
482        attributes[0].name  = pbs.ATTR_NODE_state
483        attributes[0].value = 'down'
484        attributes[0].op    = pbs.SET
485        attributes[1].name  = pbs.ATTR_NODE_note
486        attributes[1].op    = pbs.SET
487        attributes[1].value = ''
488
489        batch_list = list()
490
491        ## again a loop, now create the attrib dict list
492        for node in nodes:
493            batch_list.append(tuple([node, attributes]))
494
495        self._process(batch_list)
496 
497    def do_modify(self, nodes, note):
498        '''Modify the note on a node, override the previous note'''
499
500        if ARGS_VERBOSE:
501            _print('class:SaraNodes func:do_modify input:%s,%s' % (str(nodes), note), file=sys.stderr)
502        attributes          = pbs.new_attropl(1)
503        attributes[0].name  = pbs.ATTR_NODE_note
504        attributes[0].op    = pbs.SET
505
506        batch_list = list()
507
508        ## again a loop, now create the attrib dict list
509        for node, note in self._generate_note(nodes, note, append=False).items():
510            attributes[0].value = note
511            batch_list.append(tuple([node, attributes]))
512
513        self._process(batch_list)
514
515    def do_clear_note(self, nodes):
516        '''Clear the  note on the node(s)'''
517
518        if ARGS_VERBOSE:
519            _print('class:SaraNodes func:do_clear_note input:%s' % str(nodes), file=sys.stderr)
520        attributes          = pbs.new_attropl(1)
521        attributes[0].name  = pbs.ATTR_NODE_note
522        attributes[0].op    = pbs.SET
523        attributes[0].value = ''
524
525        batch_list = list()
526
527        ## again a loop, now create the attrib dict list
528        for node in nodes:
529            batch_list.append(tuple([node, attributes]))
530
531        self._process(batch_list)
532
533    def _process(self, batch_list):
534        '''This function execute the change to the batch server'''
535
536        if ARGS_VERBOSE:
537            _print('class:SaraNodes func:_process input:%s' % str(batch_list), file=sys.stderr)
538
539        ## Always get the pbs_server name, even in dry-run mode
540        pbs_server = pbs.pbs_default()
541        if not pbs_server:
542            _print('Could not locate a pbs server', file=sys.stderr)
543            sys.exit(1)
544
545        if ARGS_VERBOSE:
546            _print('class:SaraNodes func:_process pbs_server:%s' % pbs_server, file=sys.stderr)
547
548        ## If dry-run is not specified create a connection
549        if not ARGS_DRYRUN:
550            pbs_connection = pbs.pbs_connect(pbs_server)
551
552        ## Execute the changes
553        for node in batch_list:
554            if not ARGS_DRYRUN:
555                pbs_connection = pbs.pbs_connect(pbs_server)
556                rcode = pbs.pbs_manager(pbs_connection, pbs.MGR_CMD_SET, pbs.MGR_OBJ_NODE, node[0], node[1], 'NULL')
557                if rcode > 0:
558                    errno, text = pbs.error()
559                    _print('PBS error for node \'%s\': %s (%s)' % (node[0], text, errno), file=sys.stderr)
560            else:
561                _print("pbs.pbs_manager(pbs_connection, pbs.MGR_CMD_SET, pbs.MGR_OBJ_NODE, %s, %s, 'NULL')" % (node[0], str(node[1])))
562
563        ## Close the connection with the batch system
564        if not ARGS_DRYRUN:
565            pbs.pbs_disconnect(pbs_connection)
566
567if __name__ == '__main__':
568    ## The arguments of sara_nodes
569    parser = argparse.ArgumentParser(
570        description=HELP_DESCRIPTION,
571        epilog=HELP_EPILOG,
572    )
573    parser.add_argument('nodes', metavar='NODES', nargs='*', type=str)
574    parser.add_argument('-j', '--jobs', action='store_true', help='use job id\'s instead of nodenames')
575    parser.add_argument('-v','--verbose', action='store_true', help='enables verbose mode')
576    parser.add_argument('-n','--dry-run', action='store_true', help='enables dry-run mode')
577    parser.add_argument('-q','--quiet', action='store_true', help='enables to supress all feedback')
578    parser.add_argument('-o','--offline', metavar='NOTE', help='change state to offline with message')
579    parser.add_argument('-m','--modify', metavar='NOTE', help='change the message of a node')
580    parser.add_argument('-c','--clear', action='store_true', help='change the state to down')
581    parser.add_argument('-N','--clear-note', action='store_true', help='clear the message of a node')
582    parser.add_argument('-f','--format', metavar='FORMAT', help='change the output of sara_nodes (see footer of --help)')
583    parser.add_argument('-t','--ticket', metavar='TICKET', type=int, help='add a ticket number to a node')
584    parser.add_argument('--version', action='version', version=pbs.version)
585
586    ## Parse the arguments
587    args = parser.parse_args()
588
589    ## The options quiet, verbose and dry-run are processed first
590    if args.quiet:
591        ARGS_QUIET = True
592    if args.verbose:
593        ARGS_VERBOSE = True
594    if args.dry_run:
595        ARGS_DRYRUN = ARGS_VERBOSE = True
596
597    if ARGS_VERBOSE:
598        _print('func:__main__ checking type of operation', file=sys.stderr)
599
600    if args.nodes and args.jobs:
601        args.nodes = get_nodes(args.nodes)
602    elif args.nodes:
603        args.nodes = parse_args(args.nodes)
604
605    ## If offline, modify, clear, clear_note or ticket then initiate the SaraNodes class
606    if args.offline or args.modify or args.clear or args.clear_note or args.ticket:
607        if not args.nodes:
608            _print('You did not specify any nodes, see --help', file=sys.stderr)
609            sys.exit(1)
610
611        sn = SaraNodes()
612        if args.ticket:
613            sn.ticket = args.ticket
614
615        if args.offline:
616            if ARGS_VERBOSE:
617                _print('func:__main__ call sn.do_offline', file=sys.stderr)
618            sn.do_offline(args.nodes, args.offline)
619        elif args.modify:
620            if ARGS_VERBOSE:
621                _print('func:__main__ call sn.do_modify', file=sys.stderr)
622            sn.do_modify(args.nodes, args.modify)
623        elif args.clear:
624            if ARGS_VERBOSE:
625                _print('func:__main__ call sn.do_clear', file=sys.stderr)
626            sn.do_clear(args.nodes)
627        elif args.clear_note:
628            if ARGS_VERBOSE:
629                _print('func:__main__ call sn.do_clear_note', file=sys.stderr)
630            sn.do_clear_note(args.nodes)
631        elif args.ticket:
632            if ARGS_VERBOSE:
633                _print('func:__main__ call sn.do_modify')
634            sn.do_offline(args.nodes, '')
635    else:
636        if ARGS_DRYRUN:
637            _print('Dry-run is not available when we use PBSQuery', file=sys.stderr)
638
639        if args.format:
640            if ARGS_VERBOSE:
641                _print('func:__main__ call print_overview_format', file=sys.stderr)
642            print_overview_format(args.nodes, args.format)
643        else:
644            if ARGS_VERBOSE:
645                _print('func:__main__ call print_overview_normal', file=sys.stderr)
646            print_overview_normal(args.nodes)
647   
648    if ARGS_VERBOSE:
649        _print('func:__main__ exit', file=sys.stderr)
Note: See TracBrowser for help on using the repository browser.