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