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

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