Package ZenUtils :: Module CmdBase
[hide private]
[frames] | no frames]

Source Code for Module ZenUtils.CmdBase

  1  ########################################################################### 
  2  # 
  3  # This program is part of Zenoss Core, an open source monitoring platform. 
  4  # Copyright (C) 2007, Zenoss Inc. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify it 
  7  # under the terms of the GNU General Public License version 2 as published by 
  8  # the Free Software Foundation. 
  9  # 
 10  # For complete information please visit: http://www.zenoss.com/oss/ 
 11  # 
 12  ########################################################################### 
 13   
 14  __doc__="""CmdBase 
 15   
 16  Provide utility functions for logging and config file parsing 
 17  to command-line programs 
 18  """ 
 19   
 20  import os 
 21  import sys 
 22  import datetime 
 23  import logging 
 24   
 25  from logging import handlers 
 26  from optparse import OptionParser, SUPPRESS_HELP, NO_DEFAULT 
 27  from urllib import quote 
 28   
 29  # There is a nasty incompatibility between pkg_resources and twisted. 
 30  # This pkg_resources import works around the problem. 
 31  # See http://dev.zenoss.org/trac/ticket/3146 for details 
 32  from Products.ZenUtils.PkgResources import pkg_resources 
 33   
 34  from Products.ZenUtils.Utils import unused 
 35  unused(pkg_resources) 
 36   
37 -class DMDError: pass
38
39 -class CmdBase:
40 """ 41 Class used for all Zenoss commands 42 """ 43 44 doesLogging = True 45
46 - def __init__(self, noopts=0):
47 48 # We must import ZenossStartup at this point so that all Zenoss daemons 49 # and tools will have any ZenPack monkey-patched methods available. 50 import Products.ZenossStartup 51 unused(Products.ZenossStartup) 52 53 self.usage = "%prog [options]" 54 self.noopts = noopts 55 self.args = [] 56 self.parser = None 57 self.buildParser() 58 self.buildOptions() 59 60 self.parseOptions() 61 if self.options.configfile: 62 self.getConfigFileDefaults( self.options.configfile ) 63 64 # We've updated the parser with defaults from configs, now we need 65 # to reparse our command-line to get the correct overrides from 66 # the command-line 67 self.parseOptions() 68 if self.doesLogging: 69 self.setupLogging()
70 71
72 - def getConfigFileDefaults(self, filename):
73 """ 74 Parse a config file which has key-value pairs delimited by white space, 75 and update the parser's option defaults with these values. 76 77 @parameter filename: name of configuration file 78 @type filename: string 79 """ 80 outlines = [] 81 82 try: 83 configFile = open(filename) 84 lines = configFile.readlines() 85 configFile.close() 86 except: 87 import traceback 88 print >>sys.stderr, "WARN: unable to read config file %s -- skipping" % \ 89 filename 90 traceback.print_exc(0) 91 return 92 93 lineno = 0 94 modified = False 95 for line in lines: 96 outlines.append(line) 97 lineno += 1 98 if line.lstrip().startswith('#'): continue 99 if line.strip() == '': continue 100 101 try: 102 key, value = line.strip().split(None, 1) 103 except ValueError: 104 print >>sys.stderr, "WARN: missing value on line %d" % lineno 105 continue 106 flag= "--%s" % key 107 option= self.parser.get_option( flag ) 108 if option is None: 109 print >>sys.stderr, "INFO: Commenting out unknown option '%s' found " \ 110 "on line %d in config file" % (key, lineno) 111 #take the last line off the buffer and comment it out 112 outlines = outlines[:-1] 113 outlines.append('## %s' % line) 114 modified = True 115 continue 116 117 # NB: At this stage, optparse accepts even bogus values 118 # It will report unhappiness when it parses the arguments 119 try: 120 if option.action in [ "store_true", "store_false" ]: 121 if value in ['True', 'true']: 122 value = True 123 else: 124 value = False 125 self.parser.set_default( option.dest, value ) 126 else: 127 self.parser.set_default( option.dest, type(option.type)(value) ) 128 except: 129 print >>sys.stderr, "Bad configuration value for" \ 130 " %s at line %s, value = %s (type %s)" % ( 131 option.dest, lineno, value, option.type ) 132 133 #if we found bogus options write out the file with commented out bogus 134 #values 135 if modified: 136 configFile = file(filename, 'w') 137 configFile.writelines(outlines) 138 configFile.close()
139
140 - def checkLogpath(self):
141 """ 142 Validate the logpath is valid 143 """ 144 if not self.options.logpath: 145 return None 146 else: 147 logdir = self.options.logpath 148 if not os.path.exists(logdir): 149 # try creating the directory hierarchy if it doesn't exist... 150 try: 151 os.makedirs(logdir) 152 except OSError, ex: 153 raise SystemExit("logpath:%s doesn't exist and cannot be created" % logdir) 154 elif not os.path.isdir(logdir): 155 raise SystemExit("logpath:%s exists but is not a directory" % logdir) 156 return logdir
157
158 - def setupLogging(self):
159 """ 160 Set common logging options 161 """ 162 rlog = logging.getLogger() 163 rlog.setLevel(logging.WARN) 164 mname = self.__class__.__name__ 165 self.log = logging.getLogger("zen."+ mname) 166 zlog = logging.getLogger("zen") 167 zlog.setLevel(self.options.logseverity) 168 logdir = self.checkLogpath() 169 if logdir: 170 logfile = os.path.join(logdir, mname.lower()+".log") 171 maxBytes = self.options.maxLogKiloBytes * 1024 172 backupCount = self.options.maxBackupLogs 173 h = logging.handlers.RotatingFileHandler(logfile, maxBytes, backupCount) 174 h.setFormatter(logging.Formatter( 175 "%(asctime)s %(levelname)s %(name)s: %(message)s", 176 "%Y-%m-%d %H:%M:%S")) 177 rlog.addHandler(h) 178 else: 179 logging.basicConfig()
180 181
182 - def buildParser(self):
183 """ 184 Create the options parser 185 """ 186 if not self.parser: 187 from Products.ZenModel.ZenossInfo import ZenossInfo 188 try: 189 zinfo= ZenossInfo('') 190 version= str(zinfo.getZenossVersion()) 191 except: 192 from Products.ZenModel.ZVersion import VERSION 193 version= VERSION 194 self.parser = OptionParser(usage=self.usage, 195 version="%prog " + version )
196
197 - def buildOptions(self):
198 """ 199 Basic options setup. Other classes should call this before adding 200 more options 201 """ 202 self.buildParser() 203 if self.doesLogging: 204 self.parser.add_option('-v', '--logseverity', 205 dest='logseverity', 206 default=20, 207 type='int', 208 help='Logging severity threshold') 209 210 self.parser.add_option('--logpath',dest='logpath', 211 help='Override the default logging path') 212 213 self.parser.add_option('--maxlogsize', 214 dest='maxLogKiloBytes', 215 help='Max size of log file in KB; default 10240', 216 default=10240, 217 type='int') 218 219 self.parser.add_option('--maxbackuplogs', 220 dest='maxBackupLogs', 221 help='Max number of back up log files; default 3', 222 default=3, 223 type='int') 224 225 self.parser.add_option("-C", "--configfile", 226 dest="configfile", 227 help="Use an alternate configuration file" ) 228 229 self.parser.add_option("--genconf", 230 action="store_true", 231 default=False, 232 help="Generate a template configuration file" ) 233 234 self.parser.add_option("--genxmltable", 235 action="store_true", 236 default=False, 237 help="Generate a Docbook table showing command-line switches." ) 238 239 self.parser.add_option("--genxmlconfigs", 240 action="store_true", 241 default=False, 242 help="Generate an XML file containing command-line switches." )
243 244 245 246
247 - def pretty_print_config_comment( self, comment ):
248 """ 249 Quick and dirty pretty printer for comments that happen to be longer than can comfortably 250 be seen on the display. 251 """ 252 253 max_size= 40 254 # 255 # As a heuristic we'll accept strings that are +- text_window 256 # size in length. 257 # 258 text_window= 5 259 260 if len( comment ) <= max_size + text_window: 261 return comment 262 263 # 264 # First, take care of embedded newlines and expand them out to array entries 265 # 266 new_comment= [] 267 all_lines= comment.split( '\n' ) 268 for line in all_lines: 269 if len(line) <= max_size + text_window: 270 new_comment.append( line ) 271 continue 272 273 start_position= max_size - text_window 274 while len(line) > max_size + text_window: 275 index= line.find( ' ', start_position ) 276 if index > 0: 277 new_comment.append( line[ 0:index ] ) 278 line= line[ index: ] 279 280 else: 281 if start_position == 0: 282 # 283 # If we get here it means that the line is just one big string with no spaces 284 # in it. There's nothing that we can do except print it out. Doh! 285 # 286 new_comment.append( line ) 287 break 288 289 # 290 # Okay, haven't found anything to split on -- go back and try again 291 # 292 start_position= start_position - text_window 293 if start_position < 0: 294 start_position= 0 295 296 else: 297 new_comment.append( line ) 298 299 return "\n# ".join( new_comment )
300 301 302
303 - def generate_configs( self, parser, options ):
304 """ 305 Create a configuration file based on the long-form of the option names 306 307 @parameter parser: an optparse parser object which contains defaults, help 308 @parameter options: parsed options list containing actual values 309 """ 310 311 # 312 # Header for the configuration file 313 # 314 unused(options) 315 daemon_name= os.path.basename( sys.argv[0] ) 316 daemon_name= daemon_name.replace( '.py', '' ) 317 318 print """# 319 # Configuration file for %s 320 # 321 # To enable a particular option, uncomment the desired entry. 322 # 323 # Parameter Setting 324 # --------- -------""" % ( daemon_name ) 325 326 327 options_to_ignore= ( 'help', 'version', '', 'genconf', 'genxmltable' ) 328 329 # 330 # Create an entry for each of the command line flags 331 # 332 # NB: Ideally, this should print out only the option parser dest 333 # entries, rather than the command line options. 334 # 335 import re 336 for opt in parser.option_list: 337 if opt.help is SUPPRESS_HELP: 338 continue 339 340 # 341 # Get rid of the short version of the command 342 # 343 option_name= re.sub( r'.*/--', '', "%s" % opt ) 344 345 # 346 # And what if there's no short version? 347 # 348 option_name= re.sub( r'^--', '', "%s" % option_name ) 349 350 # 351 # Don't display anything we shouldn't be displaying 352 # 353 if option_name in options_to_ignore: 354 continue 355 356 # 357 # Find the actual value specified on the command line, if any, 358 # and display it 359 # 360 361 value= getattr( parser.values, opt.dest ) 362 363 default_value= parser.defaults.get( opt.dest ) 364 if default_value is NO_DEFAULT or default_value is None: 365 default_value= "" 366 default_string= "" 367 if default_value != "": 368 default_string= ", default: " + str( default_value ) 369 370 comment= self.pretty_print_config_comment( opt.help + default_string ) 371 372 # 373 # NB: I would prefer to use tabs to separate the parameter name 374 # and value, but I don't know that this would work. 375 # 376 print """# 377 # %s 378 #%s %s""" % ( comment, option_name, value ) 379 380 # 381 # Pretty print and exit 382 # 383 print "#" 384 sys.exit( 0 )
385 386 387
388 - def generate_xml_table( self, parser, options ):
389 """ 390 Create a Docbook table based on the long-form of the option names 391 392 @parameter parser: an optparse parser object which contains defaults, help 393 @parameter options: parsed options list containing actual values 394 """ 395 396 # 397 # Header for the configuration file 398 # 399 unused(options) 400 daemon_name= os.path.basename( sys.argv[0] ) 401 daemon_name= daemon_name.replace( '.py', '' ) 402 403 print """<?xml version="1.0" encoding="UTF-8"?> 404 405 <section version="4.0" xmlns="http://docbook.org/ns/docbook" 406 xmlns:xlink="http://www.w3.org/1999/xlink" 407 xmlns:xi="http://www.w3.org/2001/XInclude" 408 xmlns:svg="http://www.w3.org/2000/svg" 409 xmlns:mml="http://www.w3.org/1998/Math/MathML" 410 xmlns:html="http://www.w3.org/1999/xhtml" 411 xmlns:db="http://docbook.org/ns/docbook" 412 413 xml:id="%s.options" 414 > 415 416 <title>%s Options</title> 417 <para /> 418 <table frame="all"> 419 <caption>%s <indexterm><primary>Daemons</primary><secondary>%s</secondary></indexterm> options</caption> 420 <tgroup cols="2"> 421 <colspec colname="option" colwidth="1*" /> 422 <colspec colname="description" colwidth="2*" /> 423 <thead> 424 <row> 425 <entry> <para>Option</para> </entry> 426 <entry> <para>Description</para> </entry> 427 </row> 428 </thead> 429 <tbody> 430 """ % ( daemon_name, daemon_name, daemon_name, daemon_name ) 431 432 433 options_to_ignore= ( 'help', 'version', '', 'genconf', 'genxmltable' ) 434 435 # 436 # Create an entry for each of the command line flags 437 # 438 # NB: Ideally, this should print out only the option parser dest 439 # entries, rather than the command line options. 440 # 441 import re 442 for opt in parser.option_list: 443 if opt.help is SUPPRESS_HELP: 444 continue 445 446 # 447 # Create a Docbook-happy version of the option strings 448 # Yes, <arg></arg> would be better semantically, but the output 449 # just looks goofy in a table. Use literal instead. 450 # 451 all_options= '<literal>' + re.sub( r'/', '</literal>,</para> <para><literal>', "%s" % opt ) + '</literal>' 452 453 # 454 # Don't display anything we shouldn't be displaying 455 # 456 option_name= re.sub( r'.*/--', '', "%s" % opt ) 457 option_name= re.sub( r'^--', '', "%s" % option_name ) 458 if option_name in options_to_ignore: 459 continue 460 461 default_value= parser.defaults.get( opt.dest ) 462 if default_value is NO_DEFAULT or default_value is None: 463 default_value= "" 464 default_string= "" 465 if default_value != "": 466 default_string= "<para> Default: <literal>" + str( default_value ) + "</literal></para>\n" 467 468 comment= self.pretty_print_config_comment( opt.help ) 469 470 # 471 # TODO: Determine the variable name used and display the --option_name=variable_name 472 # 473 if opt.action in [ 'store_true', 'store_false' ]: 474 print """<row> 475 <entry> <para>%s</para> </entry> 476 <entry> 477 <para>%s</para> 478 %s</entry> 479 </row> 480 """ % ( all_options, comment, default_string ) 481 482 else: 483 target= '=<replaceable>' + opt.dest.lower() + '</replaceable>' 484 all_options= all_options + target 485 all_options= re.sub( r',', target + ',', all_options ) 486 print """<row> 487 <entry> <para>%s</para> </entry> 488 <entry> 489 <para>%s</para> 490 %s</entry> 491 </row> 492 """ % ( all_options, comment, default_string ) 493 494 495 496 # 497 # Close the table elements 498 # 499 print """</tbody></tgroup> 500 </table> 501 <para /> 502 </section> 503 """ 504 sys.exit( 0 )
505 506 507
508 - def generate_xml_configs( self, parser, options ):
509 """ 510 Create an XML file that can be used to create Docbook files 511 as well as used as the basis for GUI-based daemon option 512 configuration. 513 """ 514 515 # 516 # Header for the configuration file 517 # 518 unused(options) 519 daemon_name= os.path.basename( sys.argv[0] ) 520 daemon_name= daemon_name.replace( '.py', '' ) 521 522 export_date = datetime.datetime.now() 523 524 print """<?xml version="1.0" encoding="UTF-8"?> 525 526 <!-- Default daemon configuration generated on %s --> 527 <configuration id="%s" > 528 529 """ % ( export_date, daemon_name ) 530 531 options_to_ignore= ( 532 'help', 'version', '', 'genconf', 'genxmltable', 533 'genxmlconfigs', 534 ) 535 536 # 537 # Create an entry for each of the command line flags 538 # 539 # NB: Ideally, this should print out only the option parser dest 540 # entries, rather than the command line options. 541 # 542 import re 543 for opt in parser.option_list: 544 if opt.help is SUPPRESS_HELP: 545 continue 546 547 # 548 # Don't display anything we shouldn't be displaying 549 # 550 option_name= re.sub( r'.*/--', '', "%s" % opt ) 551 option_name= re.sub( r'^--', '', "%s" % option_name ) 552 if option_name in options_to_ignore: 553 continue 554 555 default_value= parser.defaults.get( opt.dest ) 556 if default_value is NO_DEFAULT or default_value is None: 557 default_string= "" 558 else: 559 default_string= str( default_value ) 560 561 # 562 # TODO: Determine the variable name used and display the --option_name=variable_name 563 # 564 if opt.action in [ 'store_true', 'store_false' ]: 565 print """ <option id="%s" type="%s" default="%s" help="%s" /> 566 """ % ( option_name, "boolean", default_string, quote(opt.help), ) 567 568 else: 569 target= opt.dest.lower() 570 print """ <option id="%s" type="%s" default="%s" target="%s" help="%s" /> 571 """ % ( option_name, opt.type, quote(default_string), target, quote(opt.help), ) 572 573 574 # 575 # Close the table elements 576 # 577 print """ 578 </configuration> 579 """ 580 sys.exit( 0 )
581 582 583
584 - def parseOptions(self):
585 """ 586 Uses the optparse parse previously populated and performs common options. 587 """ 588 589 if self.noopts: 590 args = [] 591 else: 592 import sys 593 args = sys.argv[1:] 594 (self.options, self.args) = self.parser.parse_args(args=args) 595 596 if self.options.genconf: 597 self.generate_configs( self.parser, self.options ) 598 599 if self.options.genxmltable: 600 self.generate_xml_table( self.parser, self.options ) 601 602 if self.options.genxmlconfigs: 603 self.generate_xml_configs( self.parser, self.options )
604