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

# Source Code for Module Products.ZenUtils.zenpack

      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  __doc__ = "Manage ZenPacks"
     14
     15  import os, sys
     16  import contextlib
     17  import logging
     18  import ConfigParser
     19  import subprocess
     20  from zipfile import ZipFile
     21  from StringIO import StringIO
     22
     23  import Globals
     24  import transaction
     25  from ZODB.POSException import ConflictError
     26
     27  from Products.ZenModel.ZenPack import ZenPack, ZenPackException
     28  from Products.ZenModel.ZenPack import ZenPackNeedMigrateException
     29  from Products.ZenUtils.ZenScriptBase import ZenScriptBase
     30  from Products.ZenUtils.Utils import cleanupSkins, zenPath, binPath, get_temp_dir
     31  import Products.ZenModel.ZenPackLoader as ZPL
     32  from Products.ZenModel.ZenPackLoader import CONFIG_FILE, CONFIG_SECTION_ABOUT
     33  import ZenPackCmd as EggPackCmd
     34  from Products.Zuul import getFacade
     35
     36  HIGHER_THAN_CRITICAL = 100
     37
     38 -def RemoveZenPack(dmd, packName, log=None,
     39                          skipDepsCheck=False, leaveObjects=True,
     40                          deleteFiles=True):
     41      if log:
     42          log.debug('Removing Pack "%s"' % packName)
     43      if not skipDepsCheck:
     44          for pack in dmd.ZenPackManager.packs():
     45              if packName in pack.requires:
     46                  raise ZenPackException('Pack %s depends on pack %s, '
     47                                          'not removing' % (pack.id, packName))
     48      zp = None
     49      try:
     50          zp = dmd.ZenPackManager.packs._getOb(packName)
     51      except AttributeError, ex:
     52          # Pack not in zeo, might still exist in filesystem
     53          if log:
     54              log.debug('No ZenPack named %s in zeo' % packName)
     55      if zp:
     56          try:
     57              # In 2.2 we added an additional parameter to the remove method.
     58              # Any ZenPack subclasses that haven't been updated to take the new
     59              # parameter will throw a TypeError.
     60              # The newer version of zenoss-supplied ZenPacks monkey patch
     61              # older installed versions during an upgrade so that the remove
     62              # accepts the leaveObjects method.
     63              zp.remove(dmd, leaveObjects=True)
     64          except TypeError:
     65              zp.remove(dmd)
     66          dmd.ZenPackManager.packs._delObject(packName)
     67      root = zenPath('Products', packName)
     68      if deleteFiles:
     69          if log:
     70              log.debug('Removing %s' % root)
     71          recurse = ""
     72          if os.path.isdir(root):
     73              recurse = "r"
     74          os.system('rm -%sf %s' % (recurse, root))
     75      cleanupSkins(dmd)
     76      return True
     77
     78
     79 -class ZenPackCmd(ZenScriptBase):
     80      "Manage ZenPacks"
     81
     82 -    def _verifyZepRunning(self):
     83          zep = getFacade('zep')
     84          try:
     85              zep.getConfig()
     86              return True
     87          except:
     88              pass
     89
     90 -    def run(self):
     91          "Execute the user's request"
     92          if self.args:
     93              print "Require one of --install, --remove or --list flags."
     94              self.parser.print_help()
     95              return
     96
     97          if self.options.installPackName:
     98              eggInstall = (self.options.installPackName.lower().endswith('.egg')
     99                  or os.path.exists(os.path.join(self.options.installPackName,
    100                                                  'setup.py')))
    101
    102          # files-only just lays the files down and doesn't "install"
    103          # them into zeo
    104          class ZPProxy:
    105              def __init__(self, zpId):
    106                  self.id = zpId
    107              def path(self, *parts):
    108                  return zenPath('Products', self.id, *parts)
    109          if self.options.installPackName and self.options.filesOnly:
    110              if eggInstall:
    111                  return  EggPackCmd.InstallZenPack(None,
    112                              self.options.installPackName,
    113                                  filesOnly=True)
    114              packName = self.extract(self.options.installPackName)
    115              proxy = ZPProxy(packName)
    116              for loader in (ZPL.ZPLDaemons(), ZPL.ZPLBin(), ZPL.ZPLLibExec()):
    117                  loader.load(proxy, None)
    118              return
    119          if self.options.removePackName and self.options.filesOnly:
    120              # Remove files-only is not yet supported for egg zenpacks
    121              # todo
    122              proxy = ZPProxy(self.options.removePackName)
    123              for loader in (ZPL.ZPLDaemons(), ZPL.ZPLBin(), ZPL.ZPLLibExec()):
    124                  loader.unload(proxy, None)
    125              os.system('rm -rf %s' % zenPath('Products',
    126                                              self.options.removePackName))
    127              return
    128
    129
    130
    131          self.connect()
    132
    133          if not getattr(self.dmd, 'ZenPackManager', None):
    134              raise ZenPackNeedMigrateException('Your Zenoss database appears'
    135                  ' to be out of date. Try running zenmigrate to update.')
    136
    137          if not self._verifyZepRunning() and (self.options.installPackName or self.options.removePackName):
    138              print >> sys.stderr, "Error: Required daemon zeneventserver not running."
    139              print >> sys.stderr, "Execute 'zeneventserver start' and retry the ZenPack installation."
    140              sys.exit(1)
    141
    142          if self.options.installPackName:
    143              if not self.preInstallCheck(eggInstall):
    144                  self.stop('%s not installed' % self.options.installPackName)
    145              if eggInstall:
    146                  return EggPackCmd.InstallEggAndZenPack(
    147                      self.dmd,
    148                      self.options.installPackName,
    149                      link=self.options.link,
    150                      filesOnly=False,
    151                      previousVersion= self.options.previousVersion)
    152              if os.path.isfile(self.options.installPackName):
    153                  packName = self.extract(self.options.installPackName)
    154              elif os.path.isdir(self.options.installPackName):
    155                  if self.options.link:
    156                      packName = self.linkDir(self.options.installPackName)
    157                  else:
    158                      packName = self.copyDir(self.options.installPackName)
    159              else:
    160                  self.stop('%s does not appear to be a valid file or directory.'
    161                            % self.options.installPackName)
    162              # We want to make sure all zenpacks have a skins directory and that it
    163              # is registered. The zip file may not contain a skins directory, so we
    164              # create one here if need be.  The directory should be registered
    165              # by the zenpack's __init__.py and the skin should be registered
    166              # by ZPLSkins loader.
    167              skinsSubdir = zenPath('Products', packName, 'skins', packName)
    168              if not os.path.exists(skinsSubdir):
    169                  os.makedirs(skinsSubdir, 0750)
    170              self.install(packName)
    171
    172  #        elif self.options.fetch:
    173  #            return EggPackCmd.FetchAndInstallZenPack(self.dmd,
    174  #                                self.options.fetch, self.options.fetchVers)
    175          elif self.options.removePackName:
    176              pack = self.dmd.ZenPackManager.packs._getOb(
    177                                          self.options.removePackName, None)
    178              if not pack:
    179                  raise ZenPackException('ZenPack %s is not installed.' %
    180                                          self.options.removePackName)
    181              if pack.isEggPack():
    182                  return EggPackCmd.RemoveZenPack(self.dmd,
    183                                                  self.options.removePackName)
    184              RemoveZenPack(self.dmd, self.options.removePackName, self.log)
    185
    186          elif self.options.list:
    187              for zpId in self.dmd.ZenPackManager.packs.objectIds():
    188                  try:
    189                      zp = self.dmd.ZenPackManager.packs._getOb(zpId, None)
    190                  except AttributeError:
    191                      zp = None
    192                  if not zp:
    193                      desc = 'broken'
    194                  elif zp.isEggPack():
    195                      desc = zp.eggPath()
    196                  else:
    197                      desc = zp.path()
    198                  print('%s (%s)' % (zpId,  desc))
    199
    200          transaction.commit()
    201
    202
    203 -    def preInstallCheck(self, eggInstall=True):
    204          ''' Check that prerequisite zenpacks are installed.
    205          Return True if no prereqs specified or if they are present.
    206          False otherwise.
    207          '''
    208          if eggInstall:
    209              installedPacks = dict((pack.id, pack.version) \
    210                               for pack in self.dataroot.ZenPackManager.packs())
    211
    212              if self.options.installPackName.lower().endswith('.egg'):
    213                  # standard prebuilt egg
    214                  zf = ZipFile(self.options.installPackName)
    215                  if 'EGG-INFO/requires.txt' in zf.namelist():
    216                      reqZenpacks = zf.read('EGG-INFO/requires.txt').split('\n')
    217                  else:
    218                      return True
    219              else:
    220                  # source egg, no prebuilt egg-info
    221                  with get_temp_dir() as tempEggDir:
    222                      cmd = '%s setup.py egg_info -e %s' % \
    223                                                  (binPath('python'), tempEggDir)
    224                      subprocess.call(cmd, shell=True,
    225                                      stdout=open('/dev/null', 'w'),
    226                                      cwd=self.options.installPackName)
    227
    228                      eggRequires = os.path.join(tempEggDir,
    229                                      self.options.installPackName + '.egg-info',
    230                                      'requires.txt')
    231                      if os.path.isfile(eggRequires):
    232                          reqZenpacks = open(eggRequires, 'r').read().split('\n')
    233                      else:
    234                          return True
    235
    236              for req in reqZenpacks:
    237                  zpName, zpVersion = req.strip(), None
    238                  operatorPos = req.find('>=')
    239                  if operatorPos > 0:
    240                      zpName = req[:operatorPos].strip()
    241                      zpVersion = req[operatorPos+2:].strip()
    242
    243                  if zpName in installedPacks:
    244                      if zpVersion and installedPacks[zpName] < zpVersion:
    245                          self.log.error(
    246                              'Zenpack %s requires %s to be at version %s' %
    247                              (self.options.installPackName, zpName, zpVersion))
    248                          return False
    249                  else:
    250                      self.log.error('Zenpack %s requires %s %s' %
    251                            (self.options.installPackName, zpName,
    252                                    zpVersion if zpVersion else ''))
    253                      return False
    254              return True
    255
    256          if os.path.isfile(self.options.installPackName):
    257              zf = ZipFile(self.options.installPackName)
    258              for name in zf.namelist():
    259                  if name.endswith == '/%s' % CONFIG_FILE:
    260                      sio = StringIO(zf.read(name))
    261              else:
    262                  return True
    263          else:
    264              name = os.path.join(self.options.installPackName, CONFIG_FILE)
    265              if os.path.isfile(name):
    266                  fp = open(name)
    267                  sio = StringIO(fp.read())
    268                  fp.close()
    269              else:
    270                  return True
    271
    272          parser = ConfigParser.SafeConfigParser()
    273          parser.readfp(sio, name)
    274          if parser.has_section(CONFIG_SECTION_ABOUT) \
    275              and parser.has_option(CONFIG_SECTION_ABOUT, 'requires'):
    276              requires = eval(parser.get(CONFIG_SECTION_ABOUT, 'requires'))
    277              if not isinstance(requires, list):
    278                  requires = [zp.strip() for zp in requires.split(',')]
    279              missing = [zp for zp in requires
    280                      if zp not in self.dataroot.ZenPackManager.packs.objectIds()]
    281              if missing:
    282                  self.log.error('ZenPack %s was not installed because'
    283                                  % self.options.installPackName
    284                                  + ' it requires the following ZenPack(s): %s'
    285                                  % ', '.join(missing))
    286                  return False
    287          return True
    288
    289
    290 -    def install(self, packName):
    291
    292          zp = None
    293          try:
    294              # hide uncatalog error messages since they do not do any harm
    295              log = logging.getLogger('Zope.ZCatalog')
    296              oldLevel = log.getEffectiveLevel()
    297              log.setLevel(HIGHER_THAN_CRITICAL)
    298              zp = self.dmd.ZenPackManager.packs._getOb(packName)
    299              self.log.info('Upgrading %s' % packName)
    300              zp.upgrade(self.app)
    301          except AttributeError:
    302              try:
    303                  module =  __import__('Products.' + packName, globals(), {}, [''])
    304                  zp = module.ZenPack(packName)
    305              except (ImportError, AttributeError), ex:
    306                  self.log.debug("Unable to find custom ZenPack (%s), "
    307                                 "defaulting to generic ZenPack",
    308                                 ex)
    309                  zp = ZenPack(packName)
    310              self.dmd.ZenPackManager.packs._setObject(packName, zp)
    311              zp = self.dmd.ZenPackManager.packs._getOb(packName)
    312              zp.install(self.app)
    313          finally:
    314              log.setLevel(oldLevel)
    315          if zp:
    316              for required in zp.requires:
    317                  try:
    318                      self.dmd.ZenPackManager.packs._getOb(required)
    319                  except:
    320                      self.log.error("Pack %s requires pack %s: not installing",
    321                                     packName, required)
    322                      return
    323          transaction.commit()
    324
    325 -    def extract(self, fname):
    326          "Unpack a ZenPack, and return the name"
    327          if not os.path.isfile(fname):
    328              self.stop('Unable to open file "%s"' % fname)
    329          zf = ZipFile(fname)
    330          name = zf.namelist()[0]
    331          packName = name.split('/')[0]
    332          self.log.debug('Extracting ZenPack "%s"' % packName)
    333          for name in zf.namelist():
    334              fullname = zenPath('Products', name)
    335              self.log.debug('Extracting %s' % name)
    336              if name.find('/.svn') > -1: continue
    337              if name.endswith('~'): continue
    338              if name.endswith('/'):
    339                  if not os.path.exists(fullname):
    340                      os.makedirs(fullname, 0750)
    341              else:
    342                  base = os.path.dirname(fullname)
    343                  if not os.path.isdir(base):
    344                      os.makedirs(base, 0750)
    345                  file(fullname, 'wb').write(zf.read(name))
    346          return packName
    347
    348
    349 -    def copyDir(self, srcDir):
    350          '''Copy an unzipped zenpack to the appropriate location.
    351          Return the name.
    352          '''
    353          # Normalize srcDir to not end with slash
    354          if srcDir.endswith('/'):
    355              srcDir = srcDir[:-1]
    356
    357          if not os.path.isdir(srcDir):
    358              self.stop('Specified directory does not appear to exist: %s' %
    359                          srcDir)
    360
    361          # Determine name of pack and it's destination directory
    362          packName = os.path.split(srcDir)[1]
    363          root = zenPath('Products', packName)
    364
    365          # Continue without copying if the srcDir is already in Products
    366          if os.path.exists(root) and os.path.samefile(root, srcDir):
    367              self.log.debug('Directory already in %s, not copying.',
    368                             zenPath('Products'))
    369              return packName
    370
    371          # Copy the source dir over to Products
    372          self.log.debug('Copying %s' % packName)
    373          result = os.system('cp -r %s %s' % (srcDir, zenPath('Products')))
    374          if result == -1:
    375              self.stop('Error copying %s to %s' % (srcDir, zenPath('Products')))
    376
    377          return packName
    378
    379
    380 -    def linkDir(self, srcDir):
    381          '''Symlink the srcDir into Products
    382          Return the name.
    383          '''
    384          # Normalize srcDir to not end with slash
    385          if srcDir.endswith('/'):
    386              srcDir = srcDir[:-1]
    387
    388          # Need absolute path for links
    389          srcDir = os.path.abspath(srcDir)
    390
    391          if not os.path.isdir(srcDir):
    392              self.stop('Specified directory does not appear to exist: %s' %
    393                          srcDir)
    394
    395          # Determine name of pack and it's destination directory
    396          packName = os.path.split(srcDir)[1]
    397          root = zenPath('Products', packName)
    398
    399          # Continue without copying if the srcDir is already in Products
    400          if os.path.exists(root) and os.path.samefile(root, srcDir):
    401              self.log.debug('Directory already in %s, not copying.',
    402                             zenPath('Products'))
    403              return packName
    404
    405          targetdir = zenPath("Products", packName)
    406          cmd = 'test -d %s && rm -rf %s' % (targetdir, targetdir)
    407          os.system(cmd)
    408          cmd = 'ln -s %s %s' % (srcDir, zenPath("Products"))
    409          os.system(cmd)
    410
    411          return packName
    412
    413
    414 -    def stop(self, why):
    415          self.log.error("zenpack stopped: %s", why)
    416          sys.exit(1)
    417
    418
    419 -    def buildOptions(self):
    420          self.parser.add_option('--install',
    421                                 dest='installPackName',
    422                                 default=None,
    423                                 help="Path to the ZenPack to install.")
    424  #        self.parser.add_option('--fetch',
    425  #                               dest='fetch',
    426  #                               default=None,
    427  #                               help='Name of ZenPack to retrieve from '
    428  #                                    'Zenoss.net and install.')
    429  #        self.parser.add_option('--fetchVers',
    430  #                               dest='fetchVers',
    431  #                               default=None,
    432  #                               help='Use with --fetch to specify a version'
    433  #                                    ' for the ZenPack to download and install.')
    434          self.parser.add_option('--remove', '--delete', '--uninstall', '--erase',
    435                                 dest='removePackName',
    436                                 default=None,
    437                                 help="Name of the ZenPack to remove.")
    438          self.parser.add_option('--list',
    439                                 dest='list',
    440                                 action="store_true",
    441                                 default=False,
    442                                 help='List installed ZenPacks')
    443          self.parser.add_option('--link',
    444                                 dest='link',
    445                                 action="store_true",
    446                                 default=False,
    447                                 help="Install the ZenPack in place, without "
    448                                          "copying into $ZENHOME/ZenPacks.")
    449          self.parser.add_option('--files-only',
    450                                 dest='filesOnly',
    451                                 action="store_true",
    452                                 default=False,
    453                                 help='Install the ZenPack files onto the '
    454                                          'filesystem, but do not install the '
    455                                          'ZenPack into Zenoss.')
    456          self.parser.add_option('--previousversion',
    457                                 dest='previousVersion',
    458                                 default=None,
    459                                 help="Previous version of the zenpack;"
    460                                      ' used during upgrades')
    461          self.parser.prog = "zenpack"
    462          ZenScriptBase.buildOptions(self)
    463
    464  if __name__ == '__main__':
    465      logging.basicConfig()
    466      log = logging.getLogger('zen.ZenPackCmd')
    467      try:
    468          zp = ZenPackCmd()
    469          zp.run()
    470      except ConflictError:
    471          raise
    472      except SystemExit as e:
    473          if e.code:
    474              sys.exit(e.code)
    475      except:
    476          log.exception('zenpack command failed')
    477          sys.exit(-1)
    478