1
2
3
4
5
6
7
8
9
10
11
12
13
14 __doc__ = """ZenPack
15 ZenPacks base definitions
16 """
17
18 import exceptions
19 import string
20 import subprocess
21 import os
22 import sys
23
24 from Globals import InitializeClass
25 from Products.ZenModel.ZenModelRM import ZenModelRM
26 from Products.ZenRelations.RelSchema import *
27 from Products.ZenUtils.Utils import importClass, zenPath
28 from Products.ZenUtils.Version import getVersionTupleFromString
29 from Products.ZenUtils.Version import Version as VersionBase
30 from Products.ZenUtils.PkgResources import pkg_resources
31 from Products.ZenModel.ZenPackLoader import *
32 from Products.ZenWidgets import messaging
33 from AccessControl import ClassSecurityInfo
34 from ZenossSecurity import ZEN_MANAGE_DMD
35 from Acquisition import aq_parent
36 from Products.ZenModel.ZVersion import VERSION as ZENOSS_VERSION
37
38
39
42
45
48
51
54
57
60 VersionBase.__init__(self, 'Zenoss', *args, **kw)
61
62
64 """
65 Given a list of objects, return the sorted list of unique objects
66 where uniqueness is based on the getPrimaryPath() results.
67
68 @param objs: list of objects
69 @type objs: list of objects
70 @return: sorted list of objects
71 @rtype: list of objects
72 """
73
74 def compare(x, y):
75 """
76 Comparison function based on getPrimaryPath()
77
78 @param x: object
79 @type x: object
80 @param y: object
81 @type y: object
82 @return: cmp-style return code
83 @rtype: numeric
84 """
85 return cmp(x.getPrimaryPath(), y.getPrimaryPath())
86
87 objs.sort(compare)
88 result = []
89 for obj in objs:
90 for alreadyInList in result:
91 path = alreadyInList.getPrimaryPath()
92 if obj.getPrimaryPath()[:len(path)] == path:
93 break
94 else:
95 result.append(obj)
96 return result
97
98
100 """
101 Base class for defining migration methods
102 """
103 version = Version(0, 0, 0)
104
106 """
107 ZenPack-specific migrate() method to be overridden
108
109 @param pack: ZenPack object
110 @type pack: ZenPack object
111 """
112 pass
113
115 """
116 ZenPack-specific recover() method to be overridden
117
118 @param pack: ZenPack object
119 @type pack: ZenPack object
120 """
121 pass
122
123
124
125
127 """
128 Base class for ZenPack migrate steps that need to switch classes of
129 datasources and reindex them. This is frequently done in migrate
130 scripts for 2.2 when ZenPacks are migrated to python eggs.
131 """
132
133 dsClass = None
134
135
136
137 oldDsModuleName = ''
138 oldDsClassName = ''
139
140 reIndex = False
141
166
167
169 """
170 The root of all ZenPacks: has no implementation,
171 but sits here to be the target of the Relation
172 """
173
174 objectPaths = None
175
176
177 version = '0.1'
178 author = ''
179 organization = ''
180 url = ''
181 license = ''
182 compatZenossVers = ''
183 prevZenPackName = ''
184 prevZenPackVersion = None
185
186
187
188 eggPack = False
189
190 requires = ()
191
192 loaders = (ZPLObject(), ZPLReport(), ZPLDaemons(), ZPLBin(), ZPLLibExec(),
193 ZPLSkins(), ZPLDataSources(), ZPLLibraries(), ZPLAbout())
194
195 _properties = ZenModelRM._properties + (
196 {'id':'objectPaths','type':'lines','mode':'w'},
197 {'id':'version', 'type':'string', 'mode':'w', 'description':'ZenPack version'},
198 {'id':'author', 'type':'string', 'mode':'w', 'description':'ZenPack author'},
199 {'id':'organization', 'type':'string', 'mode':'w',
200 'description':'Sponsoring organization for the ZenPack'},
201 {'id':'url', 'type':'string', 'mode':'w', 'description':'Homepage for the ZenPack'},
202 {'id':'license', 'type':'string', 'mode':'w',
203 'description':'Name of the license under which this ZenPack is available'},
204 {'id':'compatZenossVers', 'type':'string', 'mode':'w',
205 'description':'Which Zenoss versions can load this ZenPack'},
206 )
207
208 _relations = (
209
210
211 ('root', ToOne(ToManyCont, 'Products.ZenModel.DataRoot', 'packs')),
212 ('manager',
213 ToOne(ToManyCont, 'Products.ZenModel.ZenPackManager', 'packs')),
214 ("packables", ToMany(ToOne, "Products.ZenModel.ZenPackable", "pack")),
215 )
216
217 factory_type_information = (
218 { 'immediate_view' : 'viewPackDetail',
219 'factory' : 'manage_addZenPack',
220 'actions' :
221 (
222 { 'id' : 'viewPackDetail'
223 , 'name' : 'Detail'
224 , 'action' : 'viewPackDetail'
225 , 'permissions' : ( "Manage DMD", )
226 },
227 )
228 },
229 )
230
231 packZProperties = [
232 ]
233
234 security = ClassSecurityInfo()
235
236
237 - def __init__(self, id, title=None, buildRelations=True):
241
242
257
258
260 """
261 This is essentially an install() call except that a different method
262 is called on the loaders.
263 NB: Newer ZenPacks (egg style) do not use this upgrade method. Instead
264 the proper method is to remove(leaveObjects=True) and install again.
265 See ZenPackCmd.InstallDistAsZenPack().
266
267 @param app: ZenPack
268 @type app: ZenPack object
269 """
270 self.stopDaemons()
271 for loader in self.loaders:
272 loader.upgrade(self, app)
273 self.createZProperties(app)
274 self.migrate()
275 self.startDaemons()
276
277
278 - def remove(self, app, leaveObjects=False):
279 """
280 This prepares the ZenPack for removal but does not actually remove
281 the instance from ZenPackManager.packs This is sometimes called during
282 the course of an upgrade where the loaders' unload methods need to
283 be run.
284
285 @param app: ZenPack
286 @type app: ZenPack object
287 @param leaveObjects: remove zProperties and things?
288 @type leaveObjects: boolean
289 """
290 self.stopDaemons()
291 for loader in self.loaders:
292 loader.unload(self, app, leaveObjects)
293 if not leaveObjects:
294 self.removeZProperties(app)
295 self.removeCatalogedObjects(app)
296
297
298 - def migrate(self, previousVersion=None):
299 """
300 Migrate to a new version
301
302 @param previousVersion: previous version number
303 @type previousVersion: string
304 """
305 instances = []
306
307 root = self.path("migrate")
308 for p, ds, fs in os.walk(root):
309 for f in fs:
310 if f.endswith('.py') and not f.startswith("__"):
311 path = os.path.join(p[len(root) + 1:], f)
312 log.debug("Loading %s", path)
313 sys.path.insert(0, p)
314 try:
315 try:
316 c = importClass(path[:-3].replace("/", "."))
317 instances.append(c())
318 finally:
319 sys.path.remove(p)
320 except ImportError, ex:
321 log.exception("Problem loading migration step %s", path)
322
323 def versionCmp(migrate1, migrate2):
324 return cmp(migrate1.version, migrate2.version)
325 instances.sort(versionCmp)
326
327 migrateCutoff = getVersionTupleFromString(self.version)
328 if previousVersion:
329 migrateCutoff = getVersionTupleFromString(previousVersion)
330 recover = []
331
332 try:
333 for instance in instances:
334 if instance.version >= migrateCutoff:
335 recover.append(instance)
336 instance.migrate(self)
337 except Exception, ex:
338
339 recover.reverse()
340 for r in recover:
341 r.recover(self)
342 raise
343
344
345 - def list(self, app):
346 """
347 Show the list of loaders
348
349 @param app: ZenPack
350 @type app: ZenPack object
351 @return: list of loaders
352 @rtype: list of objects
353 """
354 result = []
355 for loader in self.loaders:
356 result.append((loader.name,
357 [item for item in loader.list(self, app)]))
358 return result
359
360
362 """
363 Create zProperties in the ZenPack's self.packZProperties
364
365 @param app: ZenPack
366 @type app: ZenPack object
367 """
368 for name, value, pType in self.packZProperties:
369 if not app.zport.dmd.Devices.hasProperty(name):
370 app.zport.dmd.Devices._setProperty(name, value, pType)
371
372
374 """
375 Remove any zProperties defined in the ZenPack
376
377 @param app: ZenPack
378 @type app: ZenPack object
379 """
380 for name, value, pType in self.packZProperties:
381 app.zport.dmd.Devices._delProperty(name)
382
383
385 """
386 Delete all objects in the zenPackPersistence catalog that are
387 associated with this zenpack.
388
389 @param app: ZenPack
390 @type app: ZenPack object
391 """
392 objects = self.getCatalogedObjects()
393 for o in objects:
394 parent = aq_parent(o)
395 if parent:
396 parent._delObject(o.id)
397
398
406
407
409 """
410 Edit a ZenPack object
411 """
412
413 if self.isEggPack():
414
415 newDeps = {}
416 depNames = REQUEST.get('dependencies', [])
417 if not isinstance(depNames, list):
418 depNames = [depNames]
419 newDeps = {}
420 for depName in depNames:
421 fieldName = 'version_%s' % depName
422 vers = REQUEST.get(fieldName, '').strip()
423 if vers and vers[0] in string.digits:
424 vers = '==' + vers
425 try:
426 req = pkg_resources.Requirement.parse(depName + vers)
427 except ValueError:
428 messaging.IMessageSender(self).sendToBrowser(
429 'Error',
430 '%s is not a valid version specification.' % vers,
431 priority=messaging.WARNING
432 )
433 return self.callZenScreen(REQUEST)
434 zp = self.dmd.ZenPackManager.packs._getOb(depName, None)
435 if not zp:
436 messaging.IMessageSender(self).sendToBrowser(
437 'Error',
438 '%s is not installed.' % depName,
439 priority=messaging.WARNING
440 )
441 return self.callZenScreen(REQUEST)
442 if not req.__contains__(zp.version):
443 messaging.IMessageSender(self).sendToBrowser(
444 'Error',
445 ('The required version for %s (%s) ' % (depName, vers) +
446 'does not match the installed version (%s).' %
447 zp.version),
448 priority=messaging.WARNING
449 )
450 return self.callZenScreen(REQUEST)
451 newDeps[depName] = vers
452 REQUEST.form[fieldName] = vers
453 self.dependencies = newDeps
454
455
456 compatZenossVers = REQUEST.form['compatZenossVers'] or ''
457 if compatZenossVers:
458 if compatZenossVers[0] in string.digits:
459 compatZenossVers = '==' + compatZenossVers
460 try:
461 req = pkg_resources.Requirement.parse(
462 'zenoss%s' % compatZenossVers)
463 except ValueError:
464 messaging.IMessageSender(self).sendToBrowser(
465 'Error',
466 ('%s is not a valid version specification for Zenoss.'
467 % compatZenossVers),
468 priority=messaging.WARNING
469 )
470 if not req.__contains__(ZENOSS_VERSION):
471 messaging.IMessageSender(self).sendToBrowser(
472 'Error',
473 ('%s does not match this version of Zenoss (%s).' %
474 (compatZenossVers, ZENOSS_VERSION)),
475 priority=messaging.WARNING
476 )
477 return self.callZenScreen(REQUEST)
478 REQUEST.form['compatZenossVers'] = compatZenossVers
479
480 result = ZenModelRM.zmanage_editProperties(self, REQUEST, redirect)
481
482 if self.isEggPack():
483 self.writeSetupValues()
484 self.buildEggInfo()
485 return result
486
487
501
502
517
518
519 security.declareProtected(ZEN_MANAGE_DMD, 'manage_exportPack')
521 """
522 Export the ZenPack to the /export directory
523
524 @param download: download to client's desktop? ('yes' vs anything else)
525 @type download: string
526 @type download: string
527 @param REQUEST: Zope REQUEST object
528 @type REQUEST: Zope REQUEST object
529 @todo: make this more modular
530 @todo: add better XML headers
531 """
532 if not self.isDevelopment():
533 msg = 'Only ZenPacks installed in development mode can be exported.'
534 if REQUEST:
535 messaging.IMessageSender(self).sendToBrowser(
536 'Error', msg, priority=messaging.WARNING)
537 return self.callZenScreen(REQUEST)
538 raise ZenPackDevelopmentModeExeption(msg)
539
540 from StringIO import StringIO
541 xml = StringIO()
542
543
544
545 xml.write("""<?xml version="1.0"?>\n""")
546 xml.write("<objects>\n")
547
548 packables = eliminateDuplicates(self.packables())
549 for obj in packables:
550
551 xml.write('<!-- %r -->\n' % (obj.getPrimaryPath(),))
552 obj.exportXml(xml,['devices','networks','pack'],True)
553 xml.write("</objects>\n")
554 path = self.path('objects')
555 if not os.path.isdir(path):
556 os.mkdir(path, 0750)
557 objects = file(os.path.join(path, 'objects.xml'), 'w')
558 objects.write(xml.getvalue())
559 objects.close()
560
561
562 path = self.path('skins')
563 if not os.path.isdir(path):
564 os.makedirs(path, 0750)
565
566
567 init = self.path('__init__.py')
568 if not os.path.isfile(init):
569 fp = file(init, 'w')
570 fp.write(
571 '''
572 import Globals
573 from Products.CMFCore.DirectoryView import registerDirectory
574 registerDirectory("skins", globals())
575 ''')
576 fp.close()
577
578 if self.isEggPack():
579
580 exportDir = zenPath('export')
581 if not os.path.isdir(exportDir):
582 os.makedirs(exportDir, 0750)
583 eggPath = self.eggPath()
584 os.chdir(eggPath)
585 if os.path.isdir(os.path.join(eggPath, 'dist')):
586 os.system('rm -rf dist/*')
587 p = subprocess.Popen('python setup.py bdist_egg',
588 stderr=sys.stderr,
589 shell=True,
590 cwd=eggPath)
591 p.wait()
592 os.system('cp dist/* %s' % exportDir)
593 exportFileName = self.eggName()
594 else:
595
596 about = self.path(CONFIG_FILE)
597 values = {}
598 parser = ConfigParser.SafeConfigParser()
599 if os.path.isfile(about):
600 try:
601 parser.read(about)
602 values = dict(parser.items(CONFIG_SECTION_ABOUT))
603 except ConfigParser.Error:
604 pass
605 current = [(p['id'], str(getattr(self, p['id'], '') or ''))
606 for p in self._properties]
607 values.update(dict(current))
608 if not parser.has_section(CONFIG_SECTION_ABOUT):
609 parser.add_section(CONFIG_SECTION_ABOUT)
610 for key, value in values.items():
611 parser.set(CONFIG_SECTION_ABOUT, key, value)
612 fp = file(about, 'w')
613 try:
614 parser.write(fp)
615 finally:
616 fp.close()
617
618 path = zenPath('export')
619 if not os.path.isdir(path):
620 os.makedirs(path, 0750)
621 from zipfile import ZipFile, ZIP_DEFLATED
622 zipFilePath = os.path.join(path, '%s.zip' % self.id)
623 zf = ZipFile(zipFilePath, 'w', ZIP_DEFLATED)
624 base = zenPath('Products')
625 for p, ds, fd in os.walk(self.path()):
626 if p.split('/')[-1].startswith('.'): continue
627 for f in fd:
628 if f.startswith('.'): continue
629 if f.endswith('.pyc'): continue
630 filename = os.path.join(p, f)
631 zf.write(filename, filename[len(base)+1:])
632 ds[:] = [d for d in ds if d[0] != '.']
633 zf.close()
634 exportFileName = '%s.zip' % self.id
635
636 if REQUEST:
637 if download == 'yes':
638 REQUEST['doDownload'] = 'yes'
639 messaging.IMessageSender(self).sendToBrowser(
640 'ZenPack Exported',
641 'ZenPack exported to $ZENHOME/export/%s' % (exportFileName)
642 )
643 return self.callZenScreen(REQUEST)
644
645 return exportFileName
646
647
677
678
680 dsClasses = []
681 for path, dirs, files in os.walk(self.path(name)):
682 dirs[:] = [d for d in dirs if not d.startswith('.')]
683 for f in files:
684 if not f.startswith('.') \
685 and f.endswith('.py') \
686 and not f == '__init__.py':
687 subPath = path[len(self.path()):]
688 parts = subPath.strip('/').split('/')
689 parts.append(f[:f.rfind('.')])
690 modName = '.'.join([self.moduleName()] + parts)
691 dsClasses.append(importClass(modName))
692 return dsClasses
693
696
699
701 """
702 Get the filenames of a ZenPack exclude .svn, .pyc and .xml files
703 """
704 filenames = []
705 for root, dirs, files in os.walk(self.path()):
706 if root.find('.svn') == -1:
707 for f in files:
708 if not f.endswith('.pyc') \
709 and not f.endswith('.xml'):
710 filenames.append('%s/%s' % (root, f))
711 return filenames
712
713
715 """
716 Return a list of daemons in the daemon subdirectory that should be
717 stopped/started before/after an install or an upgrade of the zenpack.
718 """
719 daemonsDir = os.path.join(self.path(), 'daemons')
720 if os.path.isdir(daemonsDir):
721 daemons = [f for f in os.listdir(daemonsDir)
722 if os.path.isfile(os.path.join(daemonsDir,f))]
723 else:
724 daemons = []
725 return daemons
726
727
729 """
730 Stop all the daemons provided by this pack.
731 Called before an upgrade or a removal of the pack.
732 """
733 return
734 for d in self.getDaemonNames():
735 self.About.doDaemonAction(d, 'stop')
736
737
739 """
740 Start all the daemons provided by this pack.
741 Called after an upgrade or an install of the pack.
742 """
743 return
744 for d in self.getDaemonNames():
745 self.About.doDaemonAction(d, 'start')
746
747
749 """
750 Restart all the daemons provided by this pack.
751 Called after an upgrade or an install of the pack.
752 """
753 for d in self.getDaemonNames():
754 self.About.doDaemonAction(d, 'restart')
755
756
757 - def path(self, *parts):
758 """
759 Return the path to the ZenPack module.
760 It would be convenient to store the module name/path in the zenpack
761 object, however this would make things more complicated when the
762 name of the package under ZenPacks changed on us (do to a user edit.)
763 """
764 if self.isEggPack():
765 module = self.getModule()
766 return os.path.join(module.__path__[0], *[p.strip('/') for p in parts])
767 return zenPath('Products', self.id, *parts)
768
769
771 """
772 Return True if
773 1) the pack is an old-style ZenPack (not a Python egg)
774 or
775 2) the pack is a Python egg and is a source install (includes a
776 setup.py file)
777
778 Returns False otherwise.
779 """
780 if self.isEggPack():
781 return os.path.isfile(self.eggPath('setup.py'))
782 return True
783
784
786 """
787 Return True if this is a new-style (egg) zenpack, false otherwise
788 """
789 return self.eggPack
790
791
793 """
794 Return the importable dotted module name for this zenpack.
795 """
796 if self.isEggPack():
797 name = self.getModule().__name__
798 else:
799 name = 'Products.%s' % self.id
800 return name
801
802
803
804
805
806
807
808
810 """
811 Write appropriate values to the setup.py file
812 """
813 import Products.ZenUtils.ZenPackCmd as ZenPackCmd
814 if not self.isEggPack():
815 raise ZenPackException('Calling writeSetupValues on non-egg zenpack.')
816
817
818 packages = []
819 parts = self.id.split('.')
820 for i in range(len(parts)):
821 packages.append('.'.join(parts[:i+1]))
822
823 attrs = dict(
824 NAME=self.id,
825 VERSION=self.version,
826 AUTHOR=self.author,
827 LICENSE=self.license,
828 NAMESPACE_PACKAGES=packages[:-1],
829 PACKAGES = packages,
830 INSTALL_REQUIRES = ['%s%s' % d for d in self.dependencies.items()],
831 COMPAT_ZENOSS_VERS = self.compatZenossVers,
832 PREV_ZENPACK_NAME = self.prevZenPackName,
833 )
834 ZenPackCmd.WriteSetup(self.eggPath('setup.py'), attrs)
835
836
838 """
839 Rebuild the egg info to update dependencies, etc
840 """
841 p = subprocess.Popen('python setup.py egg_info',
842 stderr=sys.stderr,
843 shell=True,
844 cwd=self.eggPath())
845 p.wait()
846
847
849 """
850 Return the distribution that provides this zenpack
851 """
852 if not self.isEggPack():
853 raise ZenPackException('Calling getDistribution on non-egg zenpack.')
854 return pkg_resources.get_distribution(self.id)
855
856
857 - def getEntryPoint(self):
858 """
859 Return a tuple of (packName, packEntry) that comes from the
860 distribution entry map for zenoss.zenopacks.
861 """
862 if not self.isEggPack():
863 raise ZenPackException('Calling getEntryPoints on non-egg zenpack.')
864 dist = self.getDistribution()
865 entryMap = pkg_resources.get_entry_map(dist, 'zenoss.zenpacks')
866 if not entryMap or len(entryMap) > 1:
867 raise ZenPackException('A ZenPack egg must contain exactly one'
868 ' zenoss.zenpacks entry point. This egg appears to contain'
869 ' %s such entry points.' % len(entryMap))
870 packName, packEntry = entryMap.items()[0]
871 return (packName, packEntry)
872
873
875 """
876 Get the loaded module from the given entry point. if not packEntry
877 then retrieve it.
878 """
879 if not self.isEggPack():
880 raise ZenPackException('Calling getModule on non-egg zenpack.')
881 _, packEntry = self.getEntryPoint()
882 return packEntry.load()
883
884
886 """
887 Return the path to the egg supplying this zenpack
888 """
889 if not self.isEggPack():
890 raise ZenPackException('Calling eggPath on non-egg zenpack.')
891 d = self.getDistribution()
892 return os.path.join(d.location, *[p.strip('/') for p in parts])
893
894
900
901
903 """
904 Return True if the egg itself should be deleted when this ZenPack
905 is removed from Zenoss.
906 If the ZenPack code resides in $ZENHOME/ZenPacks then it is
907 deleted, otherwise it is not.
908 """
909 eggPath = self.eggPath()
910 oneFolderUp = eggPath[:eggPath.rfind('/')]
911 if oneFolderUp == zenPath('ZenPacks'):
912 delete = True
913 else:
914 delete = False
915 return delete
916
917
919 """
920 Return the name of submodule of zenpacks that contains this zenpack.
921 """
922 if not self.isEggPack():
923 raise ZenPackException('Calling getPackageName on a non-egg '
924 'zenpack')
925 modName = self.moduleName()
926 return modName.split('.')[1]
927
928
930 """
931 Return a list of installed zenpacks that could be listed as
932 dependencies for this zenpack
933 """
934 result = []
935 for zp in self.dmd.ZenPackManager.packs():
936 try:
937 if zp.id != self.id and zp.isEggPack():
938 result.append(zp)
939 except AttributeError:
940 pass
941 return result
942
943
945 """
946 Return True if the egg is located in the ZenPacks directory,
947 False otherwise.
948 """
949 zpDir = zenPath('ZenPacks') + '/'
950 eggDir = self.eggPath()
951 return eggDir.startswith(zpDir)
952
953
955 """
956 Make sure that the ZenPack can be instantiated and that it
957 is physically present on the filesystem.
958 """
959
960
961
962
963
964 return not os.path.isdir(self.path())
965
966
967
968
969
970
971
972 ZenPackBase = ZenPack
973
974 InitializeClass(ZenPack)
975