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