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
41
44
47
50
53
56
59 VersionBase.__init__(self, 'Zenoss', *args, **kw)
60
61
63 """
64 Given a list of objects, return the sorted list of unique objects
65 where uniqueness is based on the getPrimaryPath() results.
66
67 @param objs: list of objects
68 @type objs: list of objects
69 @return: sorted list of objects
70 @rtype: list of objects
71 """
72
73 def compare(x, y):
74 """
75 Comparison function based on getPrimaryPath()
76
77 @param x: object
78 @type x: object
79 @param y: object
80 @type y: object
81 @return: cmp-style return code
82 @rtype: numeric
83 """
84 return cmp(x.getPrimaryPath(), y.getPrimaryPath())
85
86 objs.sort(compare)
87 result = []
88 for obj in objs:
89 for alreadyInList in result:
90 path = alreadyInList.getPrimaryPath()
91 if obj.getPrimaryPath()[:len(path)] == path:
92 break
93 else:
94 result.append(obj)
95 return result
96
97
99 """
100 Base class for defining migration methods
101 """
102 version = Version(0, 0, 0)
103
105 """
106 ZenPack-specific migrate() method to be overridden
107
108 @param pack: ZenPack object
109 @type pack: ZenPack object
110 """
111 pass
112
114 """
115 ZenPack-specific recover() method to be overridden
116
117 @param pack: ZenPack object
118 @type pack: ZenPack object
119 """
120 pass
121
122
123
124
126 """
127 Base class for ZenPack migrate steps that need to switch classes of
128 datasources and reindex them. This is frequently done in migrate
129 scripts for 2.2 when ZenPacks are migrated to python eggs.
130 """
131
132 dsClass = None
133
134
135
136 oldDsModuleName = ''
137 oldDsClassName = ''
138
139 reIndex = False
140
165
166
168 """
169 The root of all ZenPacks: has no implementation,
170 but sits here to be the target of the Relation
171 """
172
173 objectPaths = None
174
175
176 version = '0.1'
177 author = ''
178 organization = ''
179 url = ''
180 license = ''
181 compatZenossVers = ''
182 prevZenPackName = ''
183 prevZenPackVersion = None
184
185
186
187 eggPack = False
188
189 requires = ()
190
191 loaders = (ZPLObject(), ZPLReport(), ZPLDaemons(), ZPLBin(), ZPLLibExec(),
192 ZPLSkins(), ZPLDataSources(), ZPLLibraries(), ZPLAbout())
193
194 _properties = ZenModelRM._properties + (
195 {'id':'objectPaths','type':'lines','mode':'w'},
196 {'id':'version', 'type':'string', 'mode':'w', 'description':'ZenPack version'},
197 {'id':'author', 'type':'string', 'mode':'w', 'description':'ZenPack author'},
198 {'id':'organization', 'type':'string', 'mode':'w',
199 'description':'Sponsoring organization for the ZenPack'},
200 {'id':'url', 'type':'string', 'mode':'w', 'description':'Homepage for the ZenPack'},
201 {'id':'license', 'type':'string', 'mode':'w',
202 'description':'Name of the license under which this ZenPack is available'},
203 {'id':'compatZenossVers', 'type':'string', 'mode':'w',
204 'description':'Which Zenoss versions can load this ZenPack'},
205 )
206
207 _relations = (
208
209
210 ('root', ToOne(ToManyCont, 'Products.ZenModel.DataRoot', 'packs')),
211 ('manager',
212 ToOne(ToManyCont, 'Products.ZenModel.ZenPackManager', 'packs')),
213 ("packables", ToMany(ToOne, "Products.ZenModel.ZenPackable", "pack")),
214 )
215
216 factory_type_information = (
217 { 'immediate_view' : 'viewPackDetail',
218 'factory' : 'manage_addZenPack',
219 'actions' :
220 (
221 { 'id' : 'viewPackDetail'
222 , 'name' : 'Detail'
223 , 'action' : 'viewPackDetail'
224 , 'permissions' : ( "Manage DMD", )
225 },
226 )
227 },
228 )
229
230 packZProperties = [
231 ]
232
233 security = ClassSecurityInfo()
234
235
236 - def __init__(self, id, title=None, buildRelations=True):
240
241
256
257
259 """
260 This is essentially an install() call except that a different method
261 is called on the loaders.
262 NB: Newer ZenPacks (egg style) do not use this upgrade method. Instead
263 the proper method is to remove(leaveObjects=True) and install again.
264 See ZenPackCmd.InstallDistAsZenPack().
265
266 @param app: ZenPack
267 @type app: ZenPack object
268 """
269 self.stopDaemons()
270 for loader in self.loaders:
271 loader.upgrade(self, app)
272 self.createZProperties(app)
273 self.migrate()
274 self.startDaemons()
275
276
277 - def remove(self, app, leaveObjects=False):
278 """
279 This prepares the ZenPack for removal but does not actually remove
280 the instance from ZenPackManager.packs This is sometimes called during
281 the course of an upgrade where the loaders' unload methods need to
282 be run.
283
284 @param app: ZenPack
285 @type app: ZenPack object
286 @param leaveObjects: remove zProperties and things?
287 @type leaveObjects: boolean
288 """
289 self.stopDaemons()
290 for loader in self.loaders:
291 loader.unload(self, app, leaveObjects)
292 if not leaveObjects:
293 self.removeZProperties(app)
294 self.removeCatalogedObjects(app)
295
296
297 - def migrate(self, previousVersion=None):
298 """
299 Migrate to a new version
300
301 @param previousVersion: previous version number
302 @type previousVersion: string
303 """
304 instances = []
305
306 root = self.path("migrate")
307 for p, ds, fs in os.walk(root):
308 for f in fs:
309 if f.endswith('.py') and not f.startswith("__"):
310 path = os.path.join(p[len(root) + 1:], f)
311 log.debug("Loading %s", path)
312 sys.path.insert(0, p)
313 try:
314 try:
315 c = importClass(path[:-3].replace("/", "."))
316 instances.append(c())
317 finally:
318 sys.path.remove(p)
319 except ImportError, ex:
320 log.exception("Problem loading migration step %s", path)
321
322 def versionCmp(migrate1, migrate2):
323 return cmp(migrate1.version, migrate2.version)
324 instances.sort(versionCmp)
325
326 migrateCutoff = getVersionTupleFromString(self.version)
327 if previousVersion:
328 migrateCutoff = getVersionTupleFromString(previousVersion)
329 recover = []
330
331 try:
332 for instance in instances:
333 if instance.version >= migrateCutoff:
334 recover.append(instance)
335 instance.migrate(self)
336 except Exception, ex:
337
338 recover.reverse()
339 for r in recover:
340 r.recover(self)
341 raise
342
343
344 - def list(self, app):
345 """
346 Show the list of loaders
347
348 @param app: ZenPack
349 @type app: ZenPack object
350 @return: list of loaders
351 @rtype: list of objects
352 """
353 result = []
354 for loader in self.loaders:
355 result.append((loader.name,
356 [item for item in loader.list(self, app)]))
357 return result
358
360 """
361 Create zProperties in the ZenPack's self.packZProperties
362
363 @param app: ZenPack
364 @type app: ZenPack object
365 """
366
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
649 """
650 Download the already exported zenpack from $ZENHOME/export
651
652 @param REQUEST: Zope REQUEST object
653 @type REQUEST: Zope REQUEST object
654 """
655 if self.isEggPack():
656 filename = self.eggName()
657 else:
658 filename = '%s.zip' % self.id
659 path = os.path.join(zenPath('export'), filename)
660 if os.path.isfile(path):
661 REQUEST.RESPONSE.setHeader('content-type', 'application/zip')
662 REQUEST.RESPONSE.setHeader('content-disposition',
663 'attachment; filename=%s' %
664 filename)
665 zf = file(path, 'r')
666 try:
667 REQUEST.RESPONSE.write(zf.read())
668 finally:
669 zf.close()
670 else:
671 messaging.IMessageSender(self).sendToBrowser(
672 'Error',
673 'An error has occurred. The ZenPack could not be exported.',
674 priority=messaging.WARNING
675 )
676 return self.callZenScreen(REQUEST)
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 if not os.path.isdir(self.path()):
965 return True
966
967
968 try:
969 unused = self.packables()
970 except Exception:
971 return True
972
973 return False
974
975
976
977
978
979
980
981 ZenPackBase = ZenPack
982
983 InitializeClass(ZenPack)
984