Package ZenUtils :: Module ZenPackCmd
[hide private]
[frames] | no frames]

Source Code for Module ZenUtils.ZenPackCmd

   1  ########################################################################### 
   2  # 
   3  # This program is part of Zenoss Core, an open source monitoring platform. 
   4  # Copyright (C) 2008, 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__ = "Manage ZenPacks" 
  15   
  16  import Globals 
  17  from Products.ZenUtils.ZenScriptBase import ZenScriptBase 
  18  from Products.ZenUtils.Utils import cleanupSkins, zenPath, binPath, getObjByPath 
  19   
  20  from Products.ZenModel.ZenPack import ZenPackException, \ 
  21                                          ZenPackNotFoundException, \ 
  22                                          ZenPackNeedMigrateException 
  23  from Products.ZenModel.ZenPack import ZenPackDependentsException 
  24  from Products.ZenModel.ZenPack import ZenPack 
  25  from Products.ZenUtils.PkgResources import pkg_resources 
  26  import Products.ZenModel.ZenPackLoader as ZPL 
  27  import zenpack as oldzenpack 
  28  import transaction 
  29  import os, sys 
  30  import shutil 
  31  import string 
  32  import tempfile 
  33  import subprocess 
  34  import socket 
  35  import logging 
  36  import zExceptions 
  37   
  38   
  39  log = logging.getLogger('zen.ZenPackCMD') 
  40   
  41  #import zenpacksupport 
  42   
  43  FQDN = socket.getfqdn() 
  44   
  45  ZEN_PACK_INDEX_URL = '' 
  46   
  47  # All ZenPack eggs have to define exactly one entry point in this group. 
  48  ZENPACK_ENTRY_POINT = 'zenoss.zenpacks' 
  49   
  50  ######################################## 
  51  #   ZenPack Creation 
  52  ######################################## 
  53   
54 -def CreateZenPack(zpId, prevZenPackName=''):
55 """ 56 Create the zenpack in the filesystem. 57 The zenpack is not installed in Zenoss, it is simply created in 58 the $ZENHOME/ZenPacks directory. Usually this should be followed 59 with a "zenpack install" call. 60 zpId should already be valid, scrubbed value. 61 prevZenPackName is written to PREV_ZENPACK_NAME in setup.py. 62 """ 63 parts = zpId.split('.') 64 65 # Copy template to $ZENHOME/ZenPacks 66 srcDir = zenPath('Products', 'ZenModel', 'ZenPackTemplate') 67 devDir = zenPath('ZenPacks') 68 if not os.path.exists(devDir): 69 os.mkdir(devDir, 0750) 70 destDir = os.path.join(devDir, zpId) 71 shutil.copytree(srcDir, destDir, symlinks=False) 72 os.system('find %s -name .svn | xargs rm -rf' % destDir) 73 74 # Write setup.py 75 packages = [] 76 for i in range(len(parts)): 77 packages.append('.'.join(parts[:i+1])) 78 mapping = dict( 79 NAME = zpId, 80 VERSION = '1.0', 81 AUTHOR = '', 82 LICENSE = '', 83 NAMESPACE_PACKAGES = packages[:-1], 84 PACKAGES = packages, 85 INSTALL_REQUIRES = [], 86 COMPAT_ZENOSS_VERS = '', 87 PREV_ZENPACK_NAME = prevZenPackName, 88 ) 89 WriteSetup(os.path.join(destDir, 'setup.py'), mapping) 90 91 # Create subdirectories 92 base = destDir 93 for part in parts[:-1]: 94 base = os.path.join(base, part) 95 os.mkdir(base) 96 f = open(os.path.join(base, '__init__.py'), 'w') 97 f.write("__import__('pkg_resources').declare_namespace(__name__)\n") 98 f.close() 99 base = os.path.join(base, parts[-1]) 100 shutil.move(os.path.join(destDir, 'CONTENT'), base) 101 102 # Create the skins subdirs 103 skinsDir = os.path.join(base, 'skins', zpId) 104 os.mkdir(skinsDir) 105 106 # Stick a placeholder in the skins dir so that the egg will include 107 # the dir even if empty. 108 f = file(os.path.join(skinsDir, 'placeholder.txt'), 'w') 109 f.close() 110 111 return destDir
112 113
114 -def WriteSetup(setupPath, values):
115 """ 116 """ 117 f = file(setupPath, 'r') 118 lines = f.readlines() 119 f.close() 120 121 newLines = [] 122 for i, line in enumerate(lines): 123 if line.startswith('STOP_REPLACEMENTS'): 124 newLines += lines[i:] 125 break 126 key = line.split('=')[0].strip() 127 if values.has_key(key): 128 value = values[key] 129 if isinstance(value, basestring): 130 fmt = '%s = "%s"\n' 131 else: 132 fmt = '%s = %s\n' 133 newLines.append(fmt % (key, value)) 134 else: 135 newLines.append(line) 136 137 f = file(setupPath, 'w') 138 f.writelines(newLines) 139 f.close()
140 141
142 -def CanCreateZenPack(dmd, zpId):
143 """ 144 Return tuple (bool, string) where first element is true if a new zenpack 145 can be created with the given info and false if not. If first element 146 is True then the second part of the tuple contains the scrubbed ZenPack id. 147 If the first part is False then the second contains an explanatory 148 message. 149 """ 150 # Check if id and package looks reasonable 151 (allowable, idOrMsg) = ScrubZenPackId(zpId) 152 if allowable: 153 zpId = idOrMsg 154 else: 155 return (False, idOrMsg) 156 157 # Is the id already in use? 158 if dmd: 159 if zpId in dmd.ZenPackManager.packs.objectIds(): 160 return (False, 'A ZenPack named %s already exists.' % zpId) 161 162 # Is there another zenpack in the way? 163 # Now that zenpacks are created in $ZENHOME/ZenPacks instead of 164 # $ZENHOME/ZenPackDev this may no longer be necessary because a 165 # zp in the way should be installed and caught by the already in use 166 # check above. 167 if os.path.exists(zenPath('ZenPacks', zpId)): 168 return (False, 'A directory named %s already exists' % zpId + 169 ' in $ZENHOME/ZenPacks. Use a different name' 170 ' or remove that directory.') 171 172 return (True, idOrMsg)
173 174
175 -def ScrubZenPackId(name):
176 """ 177 If the given name conforms to ZenPack naming rules, or can easily be 178 modified to do so, then return (True, scrubbedName) where scrubbedName 179 is either name or a slightly modified name. If the given name does 180 not conform to naming rules and we can't easily modify it to do so 181 then return (False, errorMsg) where errorMsg describes why name 182 is unacceptable. 183 """ 184 parts = name.split('.') 185 186 # Remove leading dots, trailing dots, adjacent dots and strip whitespace 187 # from each part 188 parts = [p.strip() for p in parts] 189 parts = [p for p in parts if p] 190 191 # Add/fix leading 'ZenPacks' 192 if parts[0] != 'ZenPacks': 193 if parts[0].lower() == 'zenpacks': 194 parts[0] = 'ZenPacks' 195 else: 196 parts.insert(0, 'ZenPacks') 197 198 # Must be at least 3 parts 199 if len(parts) < 3: 200 return (False, 'ZenPack names must contain at least three package ' 201 'names separated by periods.') 202 203 # Each part must start with a letter 204 for p in parts: 205 if p[0] not in string.letters: 206 return (False, 'Each package name must start with a letter.') 207 208 # Only letters, numbers and underscores in each part 209 allowable = string.letters + string.digits + '_' 210 for p in parts: 211 for c in p: 212 if c not in allowable: 213 return (False, 'Package names may only contain letters, ' 214 'numbers and underscores.') 215 216 return (True, '.'.join(parts))
217 218 219 ######################################## 220 # ZenPack Installation 221 ######################################## 222
223 -class NonCriticalInstallError(Exception):
224 - def __init__(self, message):
225 Exception.__init__(self, message) 226 self.message = message
227
228 -def InstallEggAndZenPack(dmd, eggPath, link=False, 229 filesOnly=False, sendEvent=True, 230 previousVersion=None):
231 """ 232 Installs the given egg, instantiates the ZenPack, installs in 233 dmd.ZenPackManager.packs, and runs the zenpacks's install method. 234 Returns a list of ZenPacks that were installed. 235 """ 236 zenPacks = [] 237 nonCriticalErrorEncountered = False 238 try: 239 zpDists = InstallEgg(dmd, eggPath, link=link) 240 for d in zpDists: 241 try: 242 zp = InstallDistAsZenPack(dmd, 243 d, 244 eggPath, 245 link, 246 filesOnly=filesOnly, 247 previousVersion=\ 248 previousVersion) 249 zenPacks.append(zp) 250 except NonCriticalInstallError, ex: 251 nonCriticalErrorEncountered = True 252 if sendEvent: 253 ZPEvent(dmd, 3, ex.message) 254 except: 255 if sendEvent: 256 ZPEvent(dmd, 4, 'Error installing ZenPack %s' % eggPath, 257 '%s: %s' % sys.exc_info()[:2]) 258 raise 259 if sendEvent: 260 zenPackIds = [zp.id for zp in zenPacks] 261 if zenPackIds: 262 ZPEvent(dmd, 2, 'Installed ZenPacks %s' % ','.join(zenPackIds)) 263 elif not nonCriticalErrorEncountered: 264 ZPEvent(dmd, 4, 'Unable to install %s' % eggPath) 265 return zenPacks
266 267
268 -def InstallEgg(dmd, eggPath, link=False):
269 """ 270 Install the given egg and add to the current working set. 271 This does not install the egg as a ZenPack. 272 Return a list of distributions that should be installed as ZenPacks. 273 """ 274 eggPath = os.path.abspath(eggPath) 275 zenPackDir = zenPath('ZenPacks') 276 eggInZenPacksDir = eggPath.startswith(zenPackDir + '/') 277 278 # Make sure $ZENHOME/ZenPacks exists 279 CreateZenPacksDir() 280 281 # Install the egg 282 if link: 283 cmd = ('%s setup.py develop ' % binPath('python') + 284 '--site-dirs=%s ' % zenPackDir + 285 '-d %s' % zenPackDir) 286 p = subprocess.Popen(cmd, 287 stdout=subprocess.PIPE, 288 stderr=subprocess.PIPE, 289 shell=True, 290 cwd=eggPath) 291 out, err = p.communicate() 292 p.wait() 293 if p.returncode: 294 raise ZenPackException('Error installing the egg (%s): %s' % 295 (p.returncode, err)) 296 zpDists = AddDistToWorkingSet(eggPath) 297 else: 298 zpDists = DoEasyInstall(eggPath) 299 # cmd = 'easy_install --always-unzip --site-dirs=%s -d %s %s' % ( 300 # zenPackDir, 301 # zenPackDir, 302 # eggPath) 303 # p = subprocess.Popen(cmd, 304 # stdout=subprocess.PIPE, 305 # stderr=subprocess.PIPE, 306 # shell=True) 307 # p.wait() 308 # eggName = os.path.split(eggPath)[1] 309 # eggPath = os.path.join(zenPackDir, eggName) 310 311 return zpDists
312 313 314 # def GetZenPackNamesFromEggPath(eggPath): 315 # """ 316 # Given a path to a ZenPack egg (installed or not) return the 317 # name of the ZenPack it contains. 318 # """ 319 # zpNames = [] 320 # for d in pkg_resources.find_distributions(eggPath) 321 # if d.project_name.startswith('ZenPacks.'): 322 # zpNames.append(d.project_name) 323 # return zpNames 324 325
326 -def InstallDistAsZenPack(dmd, dist, eggPath, link=False, filesOnly=False, 327 previousVersion=None):
328 """ 329 Given an installed dist, install it into Zenoss as a ZenPack. 330 Return the ZenPack instance. 331 """ 332 # Instantiate ZenPack 333 entryMap = pkg_resources.get_entry_map(dist, ZENPACK_ENTRY_POINT) 334 if not entryMap or len(entryMap) > 1: 335 raise ZenPackException('A ZenPack egg must contain exactly one' 336 ' zenoss.zenpacks entry point. This egg appears to contain' 337 ' %s such entry points.' % len(entryMap)) 338 packName, packEntry = entryMap.items()[0] 339 zenPack = None; 340 runExternalZenpack = True 341 #if zenpack with same name exists we can't load both modules 342 #installing new egg zenpack will be done in a sub process 343 existing = dmd.ZenPackManager.packs._getOb(packName, None) 344 if existing: 345 log.info("Previous ZenPack exists with same name %s" % packName) 346 if filesOnly or not existing: 347 #running files only or zenpack by same name doesn't already exists 348 # so no need to install the zenpack in an external process 349 runExternalZenpack = False 350 module = packEntry.load() 351 if hasattr(module, 'ZenPack'): 352 zenPack = module.ZenPack(packName) 353 else: 354 zenPack = ZenPack(packName) 355 zenPack.eggPack = True 356 CopyMetaDataToZenPackObject(dist, zenPack) 357 if filesOnly: 358 for loader in (ZPL.ZPLDaemons(), ZPL.ZPLBin(), ZPL.ZPLLibExec()): 359 loader.load(zenPack, None) 360 361 362 if not filesOnly: 363 # Look for an installed ZenPack to be upgraded. In this case 364 # upgraded means that it is removed before the new one is installed 365 # but that its objects are not removed and the packables are 366 # copied to the new instance. 367 existing = dmd.ZenPackManager.packs._getOb(packName, None) 368 if not existing and zenPack.prevZenPackName: 369 existing = dmd.ZenPackManager.packs._getOb( 370 zenPack.prevZenPackName, None) 371 372 deferFileDeletion = False 373 packables = [] 374 upgradingFrom = None 375 if existing: 376 upgradingFrom = existing.version 377 for p in existing.packables(): 378 packables.append(p) 379 existing.packables.removeRelation(p) 380 if existing.isEggPack(): 381 forceNoFileDeletion = existing.eggPath() == dist.location 382 RemoveZenPack(dmd, existing.id, 383 skipDepsCheck=True, leaveObjects=True, 384 forceNoFileDeletion=forceNoFileDeletion, 385 uninstallEgg=False) 386 else: 387 # Don't delete files, might still be needed for 388 # migrate scripts to be run below. 389 deferFileDeletion = True 390 oldzenpack.RemoveZenPack(dmd, existing.id, 391 skipDepsCheck=True, leaveObjects=True, 392 deleteFiles=False) 393 if runExternalZenpack: 394 log.info("installing zenpack %s; launching process" % packName) 395 cmd = [binPath('zenpack')] 396 if link: 397 cmd += ["--link"] 398 cmd += ["--install", eggPath] 399 if upgradingFrom: 400 cmd += ['--previousversion', upgradingFrom] 401 402 cmdStr = " ".join(cmd) 403 log.debug("launching sub process command: %s" % cmdStr) 404 p = subprocess.Popen(cmdStr, 405 shell=True) 406 out, err = p.communicate() 407 p.wait() 408 if p.returncode: 409 raise ZenPackException('Error installing the egg (%s): %s' % 410 (p.returncode, err)) 411 dmd._p_jar.sync() 412 else: 413 dmd.ZenPackManager.packs._setObject(packName, zenPack) 414 zenPack = dmd.ZenPackManager.packs._getOb(packName) 415 #hack because ZenPack.install is overridden by a lot of zenpacks 416 #so we can't change the signature of install to take the 417 #previousVerison 418 zenPack.prevZenPackVersion = previousVersion 419 zenPack.install(dmd) 420 zenPack.prevZenPackVersion = None 421 422 423 try: 424 zenPack = dmd.ZenPackManager.packs._getOb(packName) 425 for p in packables: 426 pId = p.getPrimaryId() 427 try: 428 #make sure packable still exists; could be deleted by a migrate 429 getObjByPath(dmd, pId) 430 log.debug("adding packable relation for id %s", pId) 431 zenPack.packables.addRelation(p) 432 except (KeyError, zExceptions.NotFound): 433 log.debug('did not find packable %s',pId) 434 if deferFileDeletion: 435 # We skipped deleting the existing files from filesystem 436 # because maybe they'd be needed in migrate scripts. 437 # Delete them now 438 oldZpDir = zenPath('Products', existing.id) 439 if os.path.islink(oldZpDir): 440 os.remove(oldZpDir) 441 else: 442 shutil.rmtree(oldZpDir) 443 except AttributeError: 444 # If this happens in the child process or during the non-upgrade 445 # flow, reraise the exception 446 if not runExternalZenpack: 447 raise 448 # This is the signature error of class-loading issues 449 # during zenpack upgrade. The final state should be okay, 450 # except that modified packables may be lost. 451 message = "There has been an error during the post-" + \ 452 "installation steps for the zenpack %s. In most " + \ 453 "cases, no further action is required. If issues " + \ 454 "persist, please reinstall this zenpack." 455 message = message % packName 456 log.warning( message ) 457 raise NonCriticalInstallError( message ) 458 459 cleanupSkins(dmd) 460 transaction.commit() 461 return zenPack
462 463
464 -def DiscoverEggs(dmd, zenPackId):
465 """ 466 Find installed eggs that provide a zenoss.zenpacks entry point. 467 Return a list of distributions whose ZenPacks need to be installed 468 or upgraded. The list is sorted into the order in which this needs to 469 happen. 470 """ 471 # Create a set of all available zenoss.zenpack entries that aren't 472 # already installed in zenoss or need to be upgraded in zenoss. 473 entries = set() 474 parse_version = pkg_resources.parse_version 475 for entry in pkg_resources.iter_entry_points(ZENPACK_ENTRY_POINT): 476 packName = entry.name 477 packVers = entry.dist.version 478 existing = dmd.ZenPackManager.packs._getOb(packName, None) 479 if existing and existing.isEggPack(): 480 # We use >= to allow migrate to be run on currently installed 481 # zenpacks whose version has been changed or for whom new 482 # migrates have been added. 483 if parse_version(packVers) >= parse_version(existing.version): 484 entries.add(entry) 485 else: 486 entries.add(entry) 487 488 # Starting with the entry representing zenPackId create a list of 489 # all entrypoints 490 491 # orderedEntries lists entries in the opposite order of that in which 492 # they need to be installed. This is simply for convenience of using 493 # .append() in code below. 494 orderedEntries = [] 495 entriesByName = dict([(e.name, e) for e in entries]) 496 497 def AddEntryAndProcessDeps(e): 498 orderedEntries.append(e) 499 for name in [r.project_name for r in e.dist.requires()]: 500 if name in [e.name for e in orderedEntries]: 501 # This entry depends on something that we've already processed. 502 # This might be a circular dependency, might not be. 503 # We are just going to bail however. This should be 504 # very unusual and the user can install deps first to work 505 # around. 506 raise ZenPackException('Unable to resolve ZenPack dependencies.' 507 ' Try installing dependencies first.') 508 if name in entriesByName: 509 # The requirement is an entry that has not yet been processed 510 # here. Add it to the list of entries to install/upgrade. 511 AddEntryAndProcessDeps(entriesByName[name]) 512 else: 513 # The requirement is not in the entries generated above. 514 # This either means that the dep is already installed (this 515 # is likely) or that easy_install missed something and the dep 516 # is not installed/available (this should be unlikely.) 517 pass
518 519 if zenPackId not in entriesByName: 520 if zenPackId in dmd.ZenPackManager.packs.objectIds(): 521 return [] 522 else: 523 raise ZenPackException('Unable to discover ZenPack named %s' % 524 zenPackId) 525 AddEntryAndProcessDeps(entriesByName[zenPackId]) 526 orderedEntries.reverse() 527 return [e.dist for e in orderedEntries] 528 529
530 -def AddDistToWorkingSet(distPath):
531 """ 532 Given the path to a dist (an egg) add it to the current working set. 533 This is basically a pkg_resources-friendly way of adding it to 534 sys.path. 535 Return a list of all distributions on distPath that appear to 536 be ZenPacks. 537 """ 538 zpDists = [] 539 for d in pkg_resources.find_distributions(distPath): 540 pkg_resources.working_set.add(d) 541 pkg_resources.require(d.project_name) 542 if d.project_name.startswith('ZenPacks.'): 543 zpDists.append(d) 544 return zpDists
545 546
547 -def ReadZenPackInfo(dist):
548 """ 549 Return a dictionary containing the egg metadata 550 """ 551 info = {} 552 if dist.has_metadata('PKG-INFO'): 553 lines = dist.get_metadata('PKG-INFO') 554 for line in pkg_resources.yield_lines(lines): 555 key, value = line.split(':', 1) 556 info[key.strip()] = value.strip() 557 if dist.has_metadata('zenpack_info'): 558 lines = dist.get_metadata('zenpack_info') 559 for line in pkg_resources.yield_lines(lines): 560 key, value = line.split(':', 1) 561 info[key.strip()] = value.strip() 562 return info
563 564
565 -def CopyMetaDataToZenPackObject(dist, pack):
566 """ 567 Copy metadata type stuff from the distribution to the zp object. 568 """ 569 # Version 570 pack.version = dist.version 571 572 # Egg Info 573 info = ReadZenPackInfo(dist) 574 pack.author = info.get('Author', '') 575 if pack.author == 'UNKNOWN': 576 pack.author = '' 577 pack.compatZenossVers = info.get('compatZenossVers', '') 578 pack.prevZenPackName = info.get('prevZenPackName', '') 579 580 # Requires 581 pack.dependencies = {} 582 for r in dist.requires(): 583 name = r.project_name 584 spec = str(r)[len(name):] 585 pack.dependencies[name] = spec
586 587
588 -def CreateZenPacksDir():
589 """ 590 Make sure $ZENHOME/ZenPacks exists 591 """ 592 zpDir = zenPath('ZenPacks') 593 if not os.path.isdir(zpDir): 594 os.mkdir(zpDir, 0750)
595 596
597 -def DoEasyInstall(eggPath):
598 """ 599 Use easy_install to install an egg from the filesystem. 600 easy_install will install the egg, but does not install it into 601 Zenoss as ZenPacks. 602 Returns a list of distributions that were installed that appear 603 to be ZenPacks. 604 """ 605 from setuptools.command import easy_install 606 607 # Make sure $ZENHOME/ZenPacks exists 608 CreateZenPacksDir() 609 610 # Create temp file for easy_install to write results to 611 _, tempPath = tempfile.mkstemp(prefix='zenpackcmd-easyinstall') 612 # eggPaths is a set of paths to eggs that were installed. We need to 613 # add them to the current workingset so we can discover their 614 # entry points. 615 eggPaths = set() 616 try: 617 # Execute the easy_install 618 args = ['--site-dirs', zenPath('ZenPacks'), 619 '-d', zenPath('ZenPacks'), 620 # '-i', ZEN_PACK_INDEX_URL, 621 '--allow-hosts', 'None', 622 '--record', tempPath, 623 '--quiet', 624 eggPath] 625 easy_install.main(args) 626 # Collect the paths for eggs that were installed 627 f = open(tempPath, 'r') 628 marker = '.egg/' 629 markerOffset = len(marker)-1 630 for l in f.readlines(): 631 i = l.find(marker) 632 if i > 0: 633 eggPaths.add(l[:i+markerOffset]) 634 finally: 635 os.remove(tempPath) 636 # Add any installed eggs to the current working set 637 zpDists = [] 638 for path in eggPaths: 639 zpDists += AddDistToWorkingSet(path) 640 return zpDists
641 642 643 ######################################## 644 # Zenoss.Net 645 ######################################## 646 647
648 -def FetchAndInstallZenPack(dmd, zenPackName, zenPackVersion='', sendEvent=True):
649 """ 650 Fetch the named zenpack and all its dependencies and install them. 651 Return a list of the ZenPacks that were installed. 652 """ 653 zenPacks = [] 654 try: 655 zpDists = FetchZenPack(zenPackName, zenPackVersion) 656 for d in zpDists: 657 zenPacks.append(InstallDistAsZenPack(dmd, d)) 658 except: 659 if sendEvent: 660 ZPEvent(dmd, 4, 'Failed to install ZenPack %s' % zenPackName, 661 '%s: %s' % sys.exc_info()[:2]) 662 raise 663 if sendEvent: 664 zenPackIds = [z.id for z in zenPacks] 665 if zenPackIds: 666 ZPEvent(dmd, 2, 'Installed ZenPacks: %s' % ', '.join(zenPackIds)) 667 if zenPackName not in zenPackIds: 668 ZPEvent(dmd, 4, 'Unable to install ZenPack %s' % zenPackName) 669 return zenPacks
670 671
672 -def FetchZenPack(zenPackName, zenPackVersion=''):
673 """ 674 Use easy_install to retrieve the given zenpack and any dependencies. 675 easy_install will install the eggs, but does not install them into 676 Zenoss as ZenPacks. 677 Return a list of distributions just installed that appear to be 678 ZenPacks. 679 680 NB: This should be refactored. It shares most of its code with 681 DoEasyInstall() 682 """ 683 from setuptools.command import easy_install 684 685 # Make sure $ZENHOME/ZenPacks exists 686 CreateZenPacksDir() 687 688 # Create temp file for easy_install to write results to 689 _, tempPath = tempfile.mkstemp(prefix='zenpackcmd-easyinstall') 690 # eggPaths is a set of paths to eggs that were installed. We need to 691 # add them to the current workingset so we can discover their 692 # entry points. 693 eggPaths = set() 694 try: 695 # Execute the easy_install 696 args = ['--site-dirs', zenPath('ZenPacks'), 697 '-d', zenPath('ZenPacks'), 698 '-i', ZEN_PACK_INDEX_URL, 699 '--allow-hosts', 'None', 700 '--record', tempPath, 701 '--quiet', 702 zenPackName] 703 easy_install.main(args) 704 # Collect the paths for eggs that were installed 705 f = open(tempPath, 'r') 706 marker = '.egg/' 707 markerOffset = len(marker)-1 708 for l in f.readlines(): 709 i = l.find(marker) 710 if i > 0: 711 eggPaths.add(l[:i+markerOffset]) 712 finally: 713 os.remove(tempPath) 714 # Add any installed eggs to the current working set 715 zpDists = [] 716 for path in eggPaths: 717 zpDists += AddDistToWorkingSet(path) 718 return zpDists
719 720
721 -def UploadZenPack(dmd, packName, project, description, znetUser, znetPass):
722 """ 723 Upload the specified zenpack to the given project. 724 Project is a string of the form 'enterprise/myproject' or 725 'community/otherproject'. 726 """ 727 zp = dmd.ZenPackManager.packs._getOb(packName, None) 728 if not zp: 729 raise ZenPackException('No ZenPack named %s' % packName) 730 731 # Export the zenpack 732 fileName = zp.manage_exportPack() 733 filePath = zenPath('export', fileName) 734 735 # Login to Zenoss.net 736 from DotNetCommunication import DotNetSession 737 session = DotNetSession() 738 userSettings = dmd.ZenUsers.getUserSettings() 739 session.login(znetUser, znetPass) 740 741 # Upload 742 zpFile = open(zenPath('export', fileName), 'r') 743 try: 744 response = session.open('%s/createRelease' % project.strip('/'), { 745 'description': description, 746 'fileStorage': zpFile, 747 }) 748 finally: 749 zpFile.close() 750 if response: 751 result = response.read() 752 if "'success':true" not in result: 753 raise ZenPackException('Upload failed') 754 else: 755 raise ZenPackException('Failed to connect to Zenoss.net') 756 return
757 758 759 ######################################## 760 # ZenPack Removal 761 ######################################## 762 763
764 -def RemoveZenPack(dmd, packName, filesOnly=False, skipDepsCheck=False, 765 leaveObjects=False, sendEvent=True, 766 forceNoFileDeletion=False, uninstallEgg=True):
767 """ 768 Remove the given ZenPack from Zenoss. 769 Whether the ZenPack will be removed from the filesystem or not 770 depends on the result of the ZenPack's shouldDeleteFilesOnRemoval method. 771 """ 772 try: 773 if filesOnly: 774 skipDepsCheck = True 775 776 # Check for dependency implications here? 777 if not skipDepsCheck: 778 deps = GetDependents(dmd, packName) 779 if deps: 780 raise ZenPackDependentsException('%s cannot be removed ' % packName + 781 'because it is required by %s' % ', '.join(deps)) 782 783 if not filesOnly: 784 # Fetch the zenpack, call its remove() and remove from packs 785 zp = None 786 try: 787 zp = dmd.ZenPackManager.packs._getOb(packName) 788 except AttributeError, ex: 789 raise ZenPackNotFoundException('No ZenPack named %s is installed' % 790 packName) 791 zp.remove(dmd, leaveObjects) 792 dmd.ZenPackManager.packs._delObject(packName) 793 transaction.commit() 794 795 # Uninstall the egg and possibly delete it 796 # If we can't find the distribution then apparently the zp egg itself is 797 # missing. Continue on with the removal and swallow the 798 # DistributionNotFound exception 799 try: 800 dist = zp.getDistribution() 801 except pkg_resources.DistributionNotFound: 802 dist = None 803 if dist: 804 # Determine deleteFiles before develop -u gets called. Once 805 # it is called the egg has problems figuring out some of it's state. 806 deleteFiles = zp.shouldDeleteFilesOnRemoval() 807 if uninstallEgg: 808 if zp.isDevelopment(): 809 zenPackDir = zenPath('ZenPacks') 810 cmd = ('%s setup.py develop -u ' 811 % binPath('python') + 812 '--site-dirs=%s ' % zenPackDir + 813 '-d %s' % zenPackDir) 814 p = subprocess.Popen(cmd, 815 stdout=subprocess.PIPE, 816 stderr=subprocess.PIPE, 817 shell=True, 818 cwd=zp.eggPath()) 819 out, err = p.communicate() 820 code = p.wait() 821 if code: 822 raise ZenPackException(err) 823 else: 824 DoEasyUninstall(packName) 825 # elif cleanupEasyInstallPth: 826 # # Do we need to call easy_install -m here? It causes problems 827 # # because it tries to install deps. Cleanup easy-install.pth 828 # # ourselves instead. 829 # # We don't want to cleanup easy-install.pth when a newer 830 # # version of the egg has already been installed (when doing 831 # # an upgrade or installing in new location.) 832 # eggLink = './%s' % zp.eggName() 833 # CleanupEasyInstallPth(eggLink) 834 if deleteFiles and not forceNoFileDeletion: 835 eggDir = zp.eggPath() 836 if os.path.islink(eggDir): 837 os.remove(eggDir) 838 else: 839 shutil.rmtree(eggDir) 840 cleanupSkins(dmd) 841 transaction.commit() 842 except: 843 if sendEvent: 844 ZPEvent(dmd, 4, 'Error removing ZenPack %s' % packName, 845 '%s: %s' % sys.exc_info()[:2]) 846 raise 847 if sendEvent: 848 ZPEvent(dmd, 2, 'Removed ZenPack %s' % packName)
849 850
851 -def DoEasyUninstall(name):
852 """ 853 Execute the easy_install command to unlink the given egg. 854 What this is really doing is switching the egg to be in 855 multiple-version mode, however this is the first step in deleting 856 an egg as described here: 857 http://peak.telecommunity.com/DevCenter/EasyInstall#uninstalling-packages 858 """ 859 from setuptools.command import easy_install 860 args = ['--site-dirs', zenPath('ZenPacks'), 861 '-d', zenPath('ZenPacks'), 862 #'--record', tempPath, 863 '--quiet', 864 '-m', 865 name] 866 easy_install.main(args)
867 868
869 -def CanRemoveZenPacks(dmd, packNames):
870 """ 871 Returns a tuple of (canRemove, otherDependents) 872 canRemove is True if the listed zenPacks have no dependents not also 873 listed in packNames, False otherwise. 874 otherDependents is a list of zenpack names not in packNames that 875 depend on one or more of the packs in packNames. 876 """ 877 unhappy = set() 878 for name in packNames: 879 deps = GetDependents(dmd, name) 880 unhappy.update(set([dep for dep in deps if dep not in packNames])) 881 return (not unhappy and True or False, list(unhappy))
882 883 884 # def CleanupEasyInstallPth(eggLink): 885 # """ 886 # Remove the entry for the given egg from the 887 # $ZENHOME/ZenPacks/easy-install.pth file. If this entry is left 888 # in place in can cause problems during the next install of the same 889 # egg. 890 # """ 891 # # Remove the path from easy-install.pth 892 # eggTail = os.path.split(eggLink)[1] 893 # easyPth = zenPath('ZenPacks', 'easy-install.pth') 894 # if os.path.isfile(easyPth): 895 # needToWrite = False 896 # newLines = [] 897 # f = open(easyPth, 'r') 898 # for line in f: 899 # if os.path.split(line.strip())[1] == eggTail: 900 # needToWrite = True 901 # else: 902 # newLines.append(line) 903 # f.close() 904 # if needToWrite: 905 # f = open(easyPth, 'w') 906 # f.writelines(newLines) 907 # f.close() 908 909
910 -def GetDependents(dmd, packName):
911 """ 912 Return a list of installed ZenPack ids that list packName as a dependency 913 """ 914 return [zp.id for zp in dmd.ZenPackManager.packs() 915 if zp.id != packName and zp.dependencies.has_key(packName)]
916 917 918 ######################################## 919 # __main__, dispatching, etc 920 ######################################## 921 922
923 -def ZPEvent(dmd, severity, summary, message=None):
924 """ 925 Send an event to Zenoss. 926 """ 927 dmd.ZenEventManager.sendEvent(dict( 928 device=FQDN, 929 eventClass='/Unknown', 930 severity=severity, 931 summary=summary, 932 message=message))
933 934
935 -class ZenPackCmd(ZenScriptBase):
936 """ 937 Utilities for creating, installing, removing ZenPacks. 938 939 NOTE: Users will probably invoke zenpack from the command line, which 940 runs zenpack.py rather than this file. zenpack.py calls functions 941 in this module when it detects that new-style (egg) ZenPacks are involved. 942 The plan is that once support for old-style (non-egg) ZenPacks is dropped 943 zenpack.py can go away and this will take its place. Until then this 944 script can be invoked directly via the zenpackcmd script if desired. 945 Invoking this script directly has the benefit of slightly better 946 progress/status output to stdout. 947 """ 948
949 - def run(self):
950 """ 951 Execute the user's request. 952 """ 953 954 self.connect() 955 def PrintInstalled(installed, eggOnly=False): 956 if installed: 957 if eggOnly: 958 names = [i['id'] for i in installed] 959 what = 'ZenPack egg' 960 else: 961 names = [i.id for i in installed] 962 what = 'ZenPack' 963 print('Installed %s%s: %s' % ( 964 what, 965 len(names) > 1 and 's' or '', 966 ', '.join(names))) 967 else: 968 print('No ZenPacks installed.')
969 970 if not getattr(self.dmd, 'ZenPackManager', None): 971 raise ZenPackNeedMigrateException('Your Zenoss database appears' 972 ' to be out of date. Try running zenmigrate to update.') 973 if self.options.eggOnly and self.options.eggPath: 974 zpDists = InstallEgg(self.dmd, self.options.eggPath, 975 link=self.options.link) 976 PrintInstalled([{'id':d.project_name} for d in zpDists], 977 eggOnly=True) 978 if self.options.eggPath: 979 installed = InstallEggAndZenPack( 980 self.dmd, self.options.eggPath, 981 link=self.options.link, 982 filesOnly=self.options.filesOnly, 983 previousVersion= self.options.previousVersion) 984 PrintInstalled(installed) 985 elif self.options.fetch: 986 installed = FetchAndInstallZenPack(self.dmd, self.options.fetch) 987 PrintInstalled(installed) 988 elif self.options.upload: 989 return UploadZenPack(self.dmd, self.options.upload, 990 self.options.znetProject, 991 self.options.uploadDesc, 992 self.options.znetUser, 993 self.options.znetPass) 994 elif self.options.removePackName: 995 try: 996 RemoveZenPack(self.dmd, self.options.removePackName) 997 print('Removed ZenPack: %s' % self.options.removePackName) 998 except ZenPackNotFoundException, e: 999 sys.stderr.write(str(e) + '\n') 1000 elif self.options.list: 1001 self.list() 1002 else: 1003 self.parser.print_help()
1004 1005
1006 - def buildOptions(self):
1007 self.parser.add_option('--install', 1008 dest='eggPath', 1009 default=None, 1010 help="name of the pack to install") 1011 # self.parser.add_option('--fetch', 1012 # dest='fetch', 1013 # default=None, 1014 # help='Name of ZenPack to retrieve from ' 1015 # 'Zenoss.net and install.') 1016 # self.parser.add_option('--fetch-vers', 1017 # dest='fetchVers', 1018 # default=None, 1019 # help='Use with --fetch to specify a version' 1020 # ' for the ZenPack to download and install.') 1021 # self.parser.add_option('--znet-user', 1022 # dest='znetUser', 1023 # default=None, 1024 # help='Use with --fetch or --upload to specify' 1025 # ' your Zenoss.net username.') 1026 # self.parser.add_option('--znet-pass', 1027 # dest='znetPass', 1028 # default=None, 1029 # help='Use with --fetch or --upload to specify' 1030 # ' your Zenoss.net password.') 1031 # self.parser.add_option('--upload', 1032 # dest='upload', 1033 # default=None, 1034 # help='Name of ZenPack to upload to ' 1035 # 'Zenoss.net') 1036 # self.parser.add_option('--znet-project', 1037 # dest='znetProject', 1038 # default=None, 1039 # help='Use with --upload to specify' 1040 # ' which Zenoss.net project to create' 1041 # ' a release on.') 1042 # self.parser.add_option('--upload-desc', 1043 # dest='uploadDesc', 1044 # default=None, 1045 # help='Use with --upload to provide' 1046 # ' a description for the new release.') 1047 self.parser.add_option('--link', 1048 dest='link', 1049 action='store_true', 1050 default=False, 1051 help='Install the ZenPack in its current ' 1052 'location, do not copy to $ZENHOME/ZenPacks. ' 1053 'Also mark ZenPack as editable. ' 1054 'This only works with source directories ' 1055 'containing setup.py files, not ' 1056 'egg files.') 1057 self.parser.add_option('--remove', 1058 dest='removePackName', 1059 default=None, 1060 help="name of the pack to remove") 1061 self.parser.add_option('--leave-objects', 1062 dest='leaveObjects', 1063 default=False, 1064 action='store_true', 1065 help="When specified with --remove then objects" 1066 ' provided by the ZenPack and those' 1067 ' depending on the ZenPack are not deleted.' 1068 ' This may result in broken objects in your' 1069 ' database unless the ZenPack is' 1070 ' reinstalled.') 1071 self.parser.add_option('--files-only', 1072 dest='filesOnly', 1073 action="store_true", 1074 default=False, 1075 help='install onto filesystem but not into ' 1076 'zenoss') 1077 self.parser.add_option('--previousversion', 1078 dest='previousVersion', 1079 default=None, 1080 help="Previous version of the zenpack;" 1081 ' used during upgrades') 1082 ZenScriptBase.buildOptions(self)
1083 1084 1085 if __name__ == '__main__': 1086 try: 1087 zp = ZenPackCmd() 1088 zp.run() 1089 except ZenPackException, e: 1090 sys.stderr.write('%s\n' % str(e)) 1091 sys.exit(-1) 1092