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