source: trunk/examples/sara_nodes.py @ 321

Last change on this file since 321 was 321, checked in by dennis, 10 years ago

Added a new feature which allows you to specify jobnumbers to see the status of the nodes

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