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 dlLink = '- <a target="_blank" href="%s/manage_download">' \
638 'Download Zenpack</a>' % self.absolute_url_path()
639 messaging.IMessageSender(self).sendToBrowser(
640 'ZenPack Exported',
641 'ZenPack exported to $ZENHOME/export/%s %s' %
642 (exportFileName, dlLink if download == 'yes' else ''),
643 messaging.CRITICAL if download == 'yes' else messaging.INFO
644 )
645 return self.callZenScreen(REQUEST)
646
647 return exportFileName
648
650 """
651 Download the already exported zenpack from $ZENHOME/export
652
653 @param REQUEST: Zope REQUEST object
654 @type REQUEST: Zope REQUEST object
655 """
656 if self.isEggPack():
657 filename = self.eggName()
658 else:
659 filename = '%s.zip' % self.id
660 path = os.path.join(zenPath('export'), filename)
661 if os.path.isfile(path):
662 REQUEST.RESPONSE.setHeader('content-type', 'application/zip')
663 REQUEST.RESPONSE.setHeader('content-disposition',
664 'attachment; filename=%s' %
665 filename)
666 zf = file(path, 'r')
667 try:
668 REQUEST.RESPONSE.write(zf.read())
669 finally:
670 zf.close()
671 else:
672 messaging.IMessageSender(self).sendToBrowser(
673 'Error',
674 'An error has occurred. The ZenPack could not be exported.',
675 priority=messaging.WARNING
676 )
677 return self.callZenScreen(REQUEST)
678
679
681 dsClasses = []
682 for path, dirs, files in os.walk(self.path(name)):
683 dirs[:] = [d for d in dirs if not d.startswith('.')]
684 for f in files:
685 if not f.startswith('.') \
686 and f.endswith('.py') \
687 and not f == '__init__.py':
688 subPath = path[len(self.path()):]
689 parts = subPath.strip('/').split('/')
690 parts.append(f[:f.rfind('.')])
691 modName = '.'.join([self.moduleName()] + parts)
692 dsClasses.append(importClass(modName))
693 return dsClasses
694
697
700
702 """
703 Get the filenames of a ZenPack exclude .svn, .pyc and .xml files
704 """
705 filenames = []
706 for root, dirs, files in os.walk(self.path()):
707 if root.find('.svn') == -1:
708 for f in files:
709 if not f.endswith('.pyc') \
710 and not f.endswith('.xml'):
711 filenames.append('%s/%s' % (root, f))
712 return filenames
713
714
716 """
717 Return a list of daemons in the daemon subdirectory that should be
718 stopped/started before/after an install or an upgrade of the zenpack.
719 """
720 daemonsDir = os.path.join(self.path(), 'daemons')
721 if os.path.isdir(daemonsDir):
722 daemons = [f for f in os.listdir(daemonsDir)
723 if os.path.isfile(os.path.join(daemonsDir,f))]
724 else:
725 daemons = []
726 return daemons
727
728
730 """
731 Stop all the daemons provided by this pack.
732 Called before an upgrade or a removal of the pack.
733 """
734 return
735 for d in self.getDaemonNames():
736 self.About.doDaemonAction(d, 'stop')
737
738
740 """
741 Start all the daemons provided by this pack.
742 Called after an upgrade or an install of the pack.
743 """
744 return
745 for d in self.getDaemonNames():
746 self.About.doDaemonAction(d, 'start')
747
748
750 """
751 Restart all the daemons provided by this pack.
752 Called after an upgrade or an install of the pack.
753 """
754 for d in self.getDaemonNames():
755 self.About.doDaemonAction(d, 'restart')
756
757
758 - def path(self, *parts):
759 """
760 Return the path to the ZenPack module.
761 It would be convenient to store the module name/path in the zenpack
762 object, however this would make things more complicated when the
763 name of the package under ZenPacks changed on us (do to a user edit.)
764 """
765 if self.isEggPack():
766 module = self.getModule()
767 return os.path.join(module.__path__[0], *[p.strip('/') for p in parts])
768 return zenPath('Products', self.id, *parts)
769
770
772 """
773 Return True if
774 1) the pack is an old-style ZenPack (not a Python egg)
775 or
776 2) the pack is a Python egg and is a source install (includes a
777 setup.py file)
778
779 Returns False otherwise.
780 """
781 if self.isEggPack():
782 return os.path.isfile(self.eggPath('setup.py'))
783 return True
784
785
787 """
788 Return True if this is a new-style (egg) zenpack, false otherwise
789 """
790 return self.eggPack
791
792
794 """
795 Return the importable dotted module name for this zenpack.
796 """
797 if self.isEggPack():
798 name = self.getModule().__name__
799 else:
800 name = 'Products.%s' % self.id
801 return name
802
803
804
805
806
807
808
809
811 """
812 Write appropriate values to the setup.py file
813 """
814 import Products.ZenUtils.ZenPackCmd as ZenPackCmd
815 if not self.isEggPack():
816 raise ZenPackException('Calling writeSetupValues on non-egg zenpack.')
817
818
819 packages = []
820 parts = self.id.split('.')
821 for i in range(len(parts)):
822 packages.append('.'.join(parts[:i+1]))
823
824 attrs = dict(
825 NAME=self.id,
826 VERSION=self.version,
827 AUTHOR=self.author,
828 LICENSE=self.license,
829 NAMESPACE_PACKAGES=packages[:-1],
830 PACKAGES = packages,
831 INSTALL_REQUIRES = ['%s%s' % d for d in self.dependencies.items()],
832 COMPAT_ZENOSS_VERS = self.compatZenossVers,
833 PREV_ZENPACK_NAME = self.prevZenPackName,
834 )
835 ZenPackCmd.WriteSetup(self.eggPath('setup.py'), attrs)
836
837
839 """
840 Rebuild the egg info to update dependencies, etc
841 """
842 p = subprocess.Popen('python setup.py egg_info',
843 stderr=sys.stderr,
844 shell=True,
845 cwd=self.eggPath())
846 p.wait()
847
848
850 """
851 Return the distribution that provides this zenpack
852 """
853 if not self.isEggPack():
854 raise ZenPackException('Calling getDistribution on non-egg zenpack.')
855 return pkg_resources.get_distribution(self.id)
856
857
858 - def getEntryPoint(self):
859 """
860 Return a tuple of (packName, packEntry) that comes from the
861 distribution entry map for zenoss.zenopacks.
862 """
863 if not self.isEggPack():
864 raise ZenPackException('Calling getEntryPoints on non-egg zenpack.')
865 dist = self.getDistribution()
866 entryMap = pkg_resources.get_entry_map(dist, 'zenoss.zenpacks')
867 if not entryMap or len(entryMap) > 1:
868 raise ZenPackException('A ZenPack egg must contain exactly one'
869 ' zenoss.zenpacks entry point. This egg appears to contain'
870 ' %s such entry points.' % len(entryMap))
871 packName, packEntry = entryMap.items()[0]
872 return (packName, packEntry)
873
874
876 """
877 Get the loaded module from the given entry point. if not packEntry
878 then retrieve it.
879 """
880 if not self.isEggPack():
881 raise ZenPackException('Calling getModule on non-egg zenpack.')
882 _, packEntry = self.getEntryPoint()
883 return packEntry.load()
884
885
887 """
888 Return the path to the egg supplying this zenpack
889 """
890 if not self.isEggPack():
891 raise ZenPackException('Calling eggPath on non-egg zenpack.')
892 d = self.getDistribution()
893 return os.path.join(d.location, *[p.strip('/') for p in parts])
894
895
901
902
904 """
905 Return True if the egg itself should be deleted when this ZenPack
906 is removed from Zenoss.
907 If the ZenPack code resides in $ZENHOME/ZenPacks then it is
908 deleted, otherwise it is not.
909 """
910 eggPath = self.eggPath()
911 oneFolderUp = eggPath[:eggPath.rfind('/')]
912 if oneFolderUp == zenPath('ZenPacks'):
913 delete = True
914 else:
915 delete = False
916 return delete
917
918
920 """
921 Return the name of submodule of zenpacks that contains this zenpack.
922 """
923 if not self.isEggPack():
924 raise ZenPackException('Calling getPackageName on a non-egg '
925 'zenpack')
926 modName = self.moduleName()
927 return modName.split('.')[1]
928
929
931 """
932 Return a list of installed zenpacks that could be listed as
933 dependencies for this zenpack
934 """
935 result = []
936 for zp in self.dmd.ZenPackManager.packs():
937 try:
938 if zp.id != self.id and zp.isEggPack():
939 result.append(zp)
940 except AttributeError:
941 pass
942 return result
943
944
946 """
947 Return True if the egg is located in the ZenPacks directory,
948 False otherwise.
949 """
950 zpDir = zenPath('ZenPacks') + '/'
951 eggDir = self.eggPath()
952 return eggDir.startswith(zpDir)
953
954
956 """
957 Make sure that the ZenPack can be instantiated and that it
958 is physically present on the filesystem.
959 """
960
961
962
963
964
965 try:
966 if not os.path.isdir(self.path()):
967 return True
968 except pkg_resources.DistributionNotFound:
969 return True
970
971
972 try:
973 unused = self.packables()
974 except Exception:
975 return True
976
977 return False
978
979
980
981
982
983
984
985 ZenPackBase = ZenPack
986
987 InitializeClass(ZenPack)
988