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

Source Code for Module Products.ZenUtils.CmdBase

  1  ############################################################################## 
  2  #  
  3  # Copyright (C) Zenoss, Inc. 2007, all rights reserved. 
  4  #  
  5  # This content is made available according to terms specified in 
  6  # License.zenoss under the directory where your Zenoss product is installed. 
  7  #  
  8  ############################################################################## 
  9   
 10   
 11  __doc__="""CmdBase 
 12   
 13  Provide utility functions for logging and config file parsing 
 14  to command-line programs 
 15  """ 
 16   
 17  import os 
 18  import sys 
 19  import datetime 
 20  import logging 
 21  import re 
 22  from copy import copy 
 23  import zope.component 
 24  from zope.traversing.adapters import DefaultTraversable 
 25  from Products.Five import zcml 
 26   
 27  from optparse import ( 
 28          OptionParser, OptionGroup, Values, Option, 
 29          SUPPRESS_HELP, NO_DEFAULT, OptionValueError, BadOptionError, 
 30      ) 
 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, zenPath, getAllParserOptionsGen 
 39  from Products.ZenUtils.GlobalConfig import _convertConfigLinesToArguments, applyGlobalConfToParser 
 40  unused(pkg_resources) 
 41   
42 -class DMDError: pass
43 44
45 -def checkLogLevel(option, opt, value):
46 if re.match(r'^\d+$', value): 47 value = int(value) 48 else: 49 intval = getattr(logging, value.upper(), None) 50 if intval: 51 value = intval 52 else: 53 raise OptionValueError('"%s" is not a valid log level.' % value) 54 55 return value
56
57 -def remove_args(argv, remove_args_novals, remove_args_vals):
58 """ 59 Removes arguments from the argument list. Arguments in 60 remove_args_novals have no arguments. Arguments in 61 remove_args_vals have arguments, either in the format 62 --arg=<val> or --arg <val>. 63 """ 64 new_args = [] 65 it = iter(argv) 66 for arg in it: 67 if arg in remove_args_novals: 68 continue 69 add_arg = True 70 for remove_arg in remove_args_vals: 71 if remove_arg == arg: 72 add_arg = False 73 it.next() # Skip the argument value 74 break 75 elif arg.startswith(remove_arg + '='): 76 add_arg = False 77 break 78 if add_arg: 79 new_args.append(arg) 80 return new_args
81
82 -class LogSeverityOption(Option):
83 TYPES = Option.TYPES + ("loglevel",) 84 TYPE_CHECKER = copy(Option.TYPE_CHECKER) 85 TYPE_CHECKER["loglevel"] = checkLogLevel
86 87
88 -class CmdBase(object):
89 """ 90 Class used for all Zenoss commands 91 """ 92 93 doesLogging = True 94
95 - def __init__(self, noopts=0, args=None):
96 zope.component.provideAdapter(DefaultTraversable, (None,)) 97 # This explicitly loads all of the products - must happen first! 98 from OFS.Application import import_products 99 import_products() 100 #make sure we aren't in debug mode 101 import Globals 102 Globals.DevelopmentMode = False 103 # We must import ZenossStartup at this point so that all Zenoss daemons 104 # and tools will have any ZenPack monkey-patched methods available. 105 import Products.ZenossStartup 106 unused(Products.ZenossStartup) 107 zcml.load_site() 108 import Products.ZenWidgets 109 load_config_override('scriptmessaging.zcml', Products.ZenWidgets) 110 111 self.usage = "%prog [options]" 112 self.noopts = noopts 113 self.inputArgs = args 114 115 # inputArgs was created to allow unit tests to pass in command line 116 # arguments and get around whatever Zope was doing to sys.argv. 117 if self.inputArgs is None: 118 self.inputArgs = sys.argv[1:] 119 120 self.parser = None 121 self.args = [] 122 123 self.buildParser() 124 self.buildOptions() 125 126 # Get defaults from global.conf. They will be overridden by 127 # daemon-specific config file or command line arguments. 128 applyGlobalConfToParser(self.parser) 129 self.parseOptions() 130 if self.options.configfile: 131 self.parser.defaults = self.getConfigFileDefaults(self.options.configfile) 132 # We've updated the parser with defaults from configs, now we need 133 # to reparse our command-line to get the correct overrides from 134 # the command-line 135 self.parseOptions() 136 137 if self.doesLogging: 138 self.setupLogging()
139 140
141 - def buildParser(self):
142 """ 143 Create the options parser 144 """ 145 if not self.parser: 146 from Products.ZenModel.ZenossInfo import ZenossInfo 147 try: 148 zinfo= ZenossInfo('') 149 version= str(zinfo.getZenossVersion()) 150 except: 151 from Products.ZenModel.ZVersion import VERSION 152 version= VERSION 153 self.parser = OptionParser(usage=self.usage, 154 version="%prog " + version, 155 option_class=LogSeverityOption)
156 157
158 - def buildOptions(self):
159 """ 160 Basic options setup. Other classes should call this before adding 161 more options 162 """ 163 self.buildParser() 164 if self.doesLogging: 165 group = OptionGroup(self.parser, "Logging Options") 166 group.add_option( 167 '-v', '--logseverity', 168 dest='logseverity', default='INFO', type='loglevel', 169 help='Logging severity threshold', 170 ) 171 group.add_option( 172 '--logpath', dest='logpath', 173 help='Override the default logging path' 174 ) 175 group.add_option( 176 '--maxlogsize', 177 dest='maxLogKiloBytes', default=10240, type='int', 178 help='Max size of log file in KB; default 10240', 179 ) 180 group.add_option( 181 '--maxbackuplogs', 182 dest='maxBackupLogs', default=3, type='int', 183 help='Max number of back up log files; default 3', 184 ) 185 self.parser.add_option_group(group) 186 187 self.parser.add_option("-C", "--configfile", 188 dest="configfile", 189 help="Use an alternate configuration file" ) 190 191 self.parser.add_option("--genconf", 192 action="store_true", 193 default=False, 194 help="Generate a template configuration file" ) 195 196 self.parser.add_option("--genxmltable", 197 action="store_true", 198 default=False, 199 help="Generate a Docbook table showing command-line switches." ) 200 201 self.parser.add_option("--genxmlconfigs", 202 action="store_true", 203 default=False, 204 help="Generate an XML file containing command-line switches." )
205 206
207 - def parseOptions(self):
208 """ 209 Uses the optparse parse previously populated and performs common options. 210 """ 211 212 if self.noopts: 213 args = [] 214 else: 215 args = self.inputArgs 216 217 (self.options, self.args) = self.parser.parse_args(args=args) 218 219 if self.options.genconf: 220 self.generate_configs( self.parser, self.options ) 221 222 if self.options.genxmltable: 223 self.generate_xml_table( self.parser, self.options ) 224 225 if self.options.genxmlconfigs: 226 self.generate_xml_configs( self.parser, self.options )
227 228
229 - def getConfigFileDefaults(self, filename):
230 # TODO: This should be refactored - duplicated code with CmdBase. 231 """ 232 Parse a config file which has key-value pairs delimited by white space, 233 and update the parser's option defaults with these values. 234 235 @parameter filename: name of configuration file 236 @type filename: string 237 """ 238 239 options = self.parser.get_default_values() 240 lines = self.loadConfigFile(filename) 241 if lines: 242 lines, errors = self.validateConfigFile(filename, lines) 243 244 args = self.getParamatersFromConfig(lines) 245 try: 246 self.parser._process_args([], args, options) 247 except (BadOptionError, OptionValueError) as err: 248 print >>sys.stderr, 'WARN: %s in config file %s' % (err, filename) 249 250 return options.__dict__
251 252
254 # Deprecated: This method is going away - it is duplicated in GlobalConfig.py 255 """ 256 Parse a config file which has key-value pairs delimited by white space, 257 and update the parser's option defaults with these values. 258 """ 259 260 filename = zenPath('etc', 'global.conf') 261 options = self.parser.get_default_values() 262 lines = self.loadConfigFile(filename) 263 if lines: 264 args = self.getParamatersFromConfig(lines) 265 266 try: 267 self.parser._process_args([], args, options) 268 except (BadOptionError, OptionValueError) as err: 269 # Ignore it, we only care about our own options as defined in the parser 270 pass 271 272 return options.__dict__
273 274
275 - def loadConfigFile(self, filename):
276 # TODO: This should be refactored - duplicated code with CmdBase. 277 """ 278 Parse a config file which has key-value pairs delimited by white space. 279 280 @parameter filename: path to the configuration file 281 @type filename: string 282 """ 283 lines = [] 284 try: 285 with open(filename) as file: 286 for line in file: 287 if line.lstrip().startswith('#') or line.strip() == '': 288 lines.append(dict(type='comment', line=line)) 289 else: 290 try: 291 # add default blank string for keys with no default value 292 # valid delimiters are space, ':' and/or '=' (see ZenUtils/config.py) 293 key, value = (re.split(r'[\s:=]+', line.strip(), 1) + ['',])[:2] 294 except ValueError: 295 lines.append(dict(type='option', line=line, key=line.strip(), value=None, option=None)) 296 else: 297 option = self.parser.get_option('--%s' % key) 298 lines.append(dict(type='option', line=line, key=key, value=value, option=option)) 299 except IOError as e: 300 errorMessage = 'WARN: unable to read config file {filename} \ 301 -- skipping. ({exceptionName}: {exception})'.format( 302 filename=filename, 303 exceptionName=e.__class__.__name__, 304 exception=e 305 ) 306 print >>sys.stderr, errorMessage 307 return [] 308 309 return lines
310 311
312 - def validateConfigFile(self, filename, lines, correctErrors=True, warnErrors=True):
313 """ 314 Validate config file lines which has key-value pairs delimited by white space, 315 and validate that the keys exist for this command's option parser. If 316 the option does not exist or has an empty value it will comment it out 317 in the config file. 318 319 @parameter filename: path to the configuration file 320 @type filename: string 321 @parameter lines: lines from config parser 322 @type lines: list 323 @parameter correctErrors: Whether or not invalid conf values should be 324 commented out. 325 @type correctErrors: boolean 326 """ 327 328 output = [] 329 errors = [] 330 validLines = [] 331 date = datetime.datetime.now().isoformat() 332 errorTemplate = '## Commenting out by config parser on %s: %%s\n' % date 333 334 for lineno, line in enumerate(lines): 335 if line['type'] == 'comment': 336 output.append(line['line']) 337 elif line['type'] == 'option': 338 if line['value'] is None: 339 errors.append((lineno + 1, 'missing value for "%s"' % line['key'])) 340 output.append(errorTemplate % 'missing value') 341 output.append('## %s' % line['line']) 342 elif line['option'] is None: 343 errors.append((lineno + 1, 'unknown option "%s"' % line['key'])) 344 output.append(errorTemplate % 'unknown option') 345 output.append('## %s' % line['line']) 346 else: 347 validLines.append(line) 348 output.append(line['line']) 349 else: 350 errors.append((lineno + 1, 'unknown line "%s"' % line['line'])) 351 output.append(errorTemplate % 'unknown line') 352 output.append('## %s' % line['line']) 353 354 if errors: 355 if correctErrors: 356 for lineno, message in errors: 357 print >>sys.stderr, 'INFO: Commenting out %s on line %d in %s' % (message, lineno, filename) 358 359 with open(filename, 'w') as file: 360 file.writelines(output) 361 362 if warnErrors: 363 for lineno, message in errors: 364 print >>sys.stderr, 'WARN: %s on line %d in %s' % (message, lineno, filename) 365 366 return validLines, errors
367 368
369 - def getParamatersFromConfig(self, lines):
370 # Deprecated: This method is going away 371 return _convertConfigLinesToArguments(self.parser, lines)
372 373
374 - def setupLogging(self):
375 """ 376 Set common logging options 377 """ 378 rlog = logging.getLogger() 379 rlog.setLevel(logging.WARN) 380 mname = self.__class__.__name__ 381 self.log = logging.getLogger("zen."+ mname) 382 zlog = logging.getLogger("zen") 383 try: 384 loglevel = int(self.options.logseverity) 385 except ValueError: 386 loglevel = getattr(logging, self.options.logseverity.upper(), logging.INFO) 387 zlog.setLevel(loglevel) 388 389 logdir = self.checkLogpath() 390 if logdir: 391 logfile = os.path.join(logdir, mname.lower()+".log") 392 maxBytes = self.options.maxLogKiloBytes * 1024 393 backupCount = self.options.maxBackupLogs 394 h = logging.handlers.RotatingFileHandler(logfile, maxBytes, backupCount) 395 h.setFormatter(logging.Formatter( 396 "%(asctime)s %(levelname)s %(name)s: %(message)s", 397 "%Y-%m-%d %H:%M:%S")) 398 rlog.addHandler(h) 399 else: 400 logging.basicConfig()
401 402
403 - def checkLogpath(self):
404 """ 405 Validate the logpath is valid 406 """ 407 if not self.options.logpath: 408 return None 409 else: 410 logdir = self.options.logpath 411 if not os.path.exists(logdir): 412 # try creating the directory hierarchy if it doesn't exist... 413 try: 414 os.makedirs(logdir) 415 except OSError, ex: 416 raise SystemExit("logpath:%s doesn't exist and cannot be created" % logdir) 417 elif not os.path.isdir(logdir): 418 raise SystemExit("logpath:%s exists but is not a directory" % logdir) 419 return logdir
420 421
422 - def pretty_print_config_comment( self, comment ):
423 """ 424 Quick and dirty pretty printer for comments that happen to be longer than can comfortably 425 be seen on the display. 426 """ 427 428 max_size= 40 429 # 430 # As a heuristic we'll accept strings that are +- text_window 431 # size in length. 432 # 433 text_window= 5 434 435 if len( comment ) <= max_size + text_window: 436 return comment 437 438 # 439 # First, take care of embedded newlines and expand them out to array entries 440 # 441 new_comment= [] 442 all_lines= comment.split( '\n' ) 443 for line in all_lines: 444 if len(line) <= max_size + text_window: 445 new_comment.append( line ) 446 continue 447 448 start_position= max_size - text_window 449 while len(line) > max_size + text_window: 450 index= line.find( ' ', start_position ) 451 if index > 0: 452 new_comment.append( line[ 0:index ] ) 453 line= line[ index: ] 454 455 else: 456 if start_position == 0: 457 # 458 # If we get here it means that the line is just one big string with no spaces 459 # in it. There's nothing that we can do except print it out. Doh! 460 # 461 new_comment.append( line ) 462 break 463 464 # 465 # Okay, haven't found anything to split on -- go back and try again 466 # 467 start_position= start_position - text_window 468 if start_position < 0: 469 start_position= 0 470 471 else: 472 new_comment.append( line ) 473 474 return "\n# ".join( new_comment )
475 476 477
478 - def generate_configs( self, parser, options ):
479 """ 480 Create a configuration file based on the long-form of the option names 481 482 @parameter parser: an optparse parser object which contains defaults, help 483 @parameter options: parsed options list containing actual values 484 """ 485 486 # 487 # Header for the configuration file 488 # 489 unused(options) 490 daemon_name= os.path.basename( sys.argv[0] ) 491 daemon_name= daemon_name.replace( '.py', '' ) 492 493 print """# 494 # Configuration file for %s 495 # 496 # To enable a particular option, uncomment the desired entry. 497 # 498 # Parameter Setting 499 # --------- -------""" % ( daemon_name ) 500 501 502 options_to_ignore= ( 'help', 'version', '', 'genconf', 'genxmltable' ) 503 504 # 505 # Create an entry for each of the command line flags 506 # 507 # NB: Ideally, this should print out only the option parser dest 508 # entries, rather than the command line options. 509 # 510 import re 511 for opt in getAllParserOptionsGen(parser): 512 if opt.help is SUPPRESS_HELP: 513 continue 514 515 # 516 # Get rid of the short version of the command 517 # 518 option_name= re.sub( r'.*/--', '', "%s" % opt ) 519 520 # 521 # And what if there's no short version? 522 # 523 option_name= re.sub( r'^--', '', "%s" % option_name ) 524 525 # 526 # Don't display anything we shouldn't be displaying 527 # 528 if option_name in options_to_ignore: 529 continue 530 531 # 532 # Find the actual value specified on the command line, if any, 533 # and display it 534 # 535 536 value= getattr( parser.values, opt.dest ) 537 538 default_value= parser.defaults.get( opt.dest ) 539 if default_value is NO_DEFAULT or default_value is None: 540 default_value= "" 541 default_string= "" 542 if default_value != "": 543 default_string= ", default: " + str( default_value ) 544 545 comment= self.pretty_print_config_comment( opt.help + default_string ) 546 547 # 548 # NB: I would prefer to use tabs to separate the parameter name 549 # and value, but I don't know that this would work. 550 # 551 print """# 552 # %s 553 #%s %s""" % ( comment, option_name, value ) 554 555 # 556 # Pretty print and exit 557 # 558 print "#" 559 sys.exit( 0 )
560 561 562
563 - def generate_xml_table( self, parser, options ):
564 """ 565 Create a Docbook table based on the long-form of the option names 566 567 @parameter parser: an optparse parser object which contains defaults, help 568 @parameter options: parsed options list containing actual values 569 """ 570 571 # 572 # Header for the configuration file 573 # 574 unused(options) 575 daemon_name= os.path.basename( sys.argv[0] ) 576 daemon_name= daemon_name.replace( '.py', '' ) 577 578 print """<?xml version="1.0" encoding="UTF-8"?> 579 580 <section version="4.0" xmlns="http://docbook.org/ns/docbook" 581 xmlns:xlink="http://www.w3.org/1999/xlink" 582 xmlns:xi="http://www.w3.org/2001/XInclude" 583 xmlns:svg="http://www.w3.org/2000/svg" 584 xmlns:mml="http://www.w3.org/1998/Math/MathML" 585 xmlns:html="http://www.w3.org/1999/xhtml" 586 xmlns:db="http://docbook.org/ns/docbook" 587 588 xml:id="%s.options" 589 > 590 591 <title>%s Options</title> 592 <para /> 593 <table frame="all"> 594 <caption>%s <indexterm><primary>Daemons</primary><secondary>%s</secondary></indexterm> options</caption> 595 <tgroup cols="2"> 596 <colspec colname="option" colwidth="1*" /> 597 <colspec colname="description" colwidth="2*" /> 598 <thead> 599 <row> 600 <entry> <para>Option</para> </entry> 601 <entry> <para>Description</para> </entry> 602 </row> 603 </thead> 604 <tbody> 605 """ % ( daemon_name, daemon_name, daemon_name, daemon_name ) 606 607 608 options_to_ignore= ( 'help', 'version', '', 'genconf', 'genxmltable' ) 609 610 # 611 # Create an entry for each of the command line flags 612 # 613 # NB: Ideally, this should print out only the option parser dest 614 # entries, rather than the command line options. 615 # 616 import re 617 for opt in getAllParserOptionsGen(parser): 618 if opt.help is SUPPRESS_HELP: 619 continue 620 621 # 622 # Create a Docbook-happy version of the option strings 623 # Yes, <arg></arg> would be better semantically, but the output 624 # just looks goofy in a table. Use literal instead. 625 # 626 all_options= '<literal>' + re.sub( r'/', '</literal>,</para> <para><literal>', "%s" % opt ) + '</literal>' 627 628 # 629 # Don't display anything we shouldn't be displaying 630 # 631 option_name= re.sub( r'.*/--', '', "%s" % opt ) 632 option_name= re.sub( r'^--', '', "%s" % option_name ) 633 if option_name in options_to_ignore: 634 continue 635 636 default_value= parser.defaults.get( opt.dest ) 637 if default_value is NO_DEFAULT or default_value is None: 638 default_value= "" 639 default_string= "" 640 if default_value != "": 641 default_string= "<para> Default: <literal>" + str( default_value ) + "</literal></para>\n" 642 643 comment= self.pretty_print_config_comment( opt.help ) 644 645 # 646 # TODO: Determine the variable name used and display the --option_name=variable_name 647 # 648 if opt.action in [ 'store_true', 'store_false' ]: 649 print """<row> 650 <entry> <para>%s</para> </entry> 651 <entry> 652 <para>%s</para> 653 %s</entry> 654 </row> 655 """ % ( all_options, comment, default_string ) 656 657 else: 658 target= '=<replaceable>' + opt.dest.lower() + '</replaceable>' 659 all_options= all_options + target 660 all_options= re.sub( r',', target + ',', all_options ) 661 print """<row> 662 <entry> <para>%s</para> </entry> 663 <entry> 664 <para>%s</para> 665 %s</entry> 666 </row> 667 """ % ( all_options, comment, default_string ) 668 669 670 671 # 672 # Close the table elements 673 # 674 print """</tbody></tgroup> 675 </table> 676 <para /> 677 </section> 678 """ 679 sys.exit( 0 )
680 681 682
683 - def generate_xml_configs( self, parser, options ):
684 """ 685 Create an XML file that can be used to create Docbook files 686 as well as used as the basis for GUI-based daemon option 687 configuration. 688 """ 689 690 # 691 # Header for the configuration file 692 # 693 unused(options) 694 daemon_name= os.path.basename( sys.argv[0] ) 695 daemon_name= daemon_name.replace( '.py', '' ) 696 697 export_date = datetime.datetime.now() 698 699 print """<?xml version="1.0" encoding="UTF-8"?> 700 701 <!-- Default daemon configuration generated on %s --> 702 <configuration id="%s" > 703 704 """ % ( export_date, daemon_name ) 705 706 options_to_ignore= ( 707 'help', 'version', '', 'genconf', 'genxmltable', 708 'genxmlconfigs', 709 ) 710 711 # 712 # Create an entry for each of the command line flags 713 # 714 # NB: Ideally, this should print out only the option parser dest 715 # entries, rather than the command line options. 716 # 717 import re 718 for opt in getAllParserOptionsGen(parser): 719 if opt.help is SUPPRESS_HELP: 720 continue 721 722 # 723 # Don't display anything we shouldn't be displaying 724 # 725 option_name= re.sub( r'.*/--', '', "%s" % opt ) 726 option_name= re.sub( r'^--', '', "%s" % option_name ) 727 if option_name in options_to_ignore: 728 continue 729 730 default_value= parser.defaults.get( opt.dest ) 731 if default_value is NO_DEFAULT or default_value is None: 732 default_string= "" 733 else: 734 default_string= str( default_value ) 735 736 # 737 # TODO: Determine the variable name used and display the --option_name=variable_name 738 # 739 if opt.action in [ 'store_true', 'store_false' ]: 740 print """ <option id="%s" type="%s" default="%s" help="%s" /> 741 """ % ( option_name, "boolean", default_string, quote(opt.help), ) 742 743 else: 744 target= opt.dest.lower() 745 print """ <option id="%s" type="%s" default="%s" target="%s" help="%s" /> 746 """ % ( option_name, opt.type, quote(default_string), target, quote(opt.help), ) 747 748 749 # 750 # Close the table elements 751 # 752 print """ 753 </configuration> 754 """ 755 sys.exit( 0 )
756