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