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

# Source Code for Module Products.ZenUtils.zenpack

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