diff --git a/CONTENT/ExampleComponent.py b/CONTENT/ExampleComponent.py new file mode 100644 index 0000000..dba2da6 --- /dev/null +++ b/CONTENT/ExampleComponent.py @@ -0,0 +1,40 @@ +from Products.ZenModel.DeviceComponent import DeviceComponent +from Products.ZenModel.ManagedEntity import ManagedEntity +from Products.ZenModel.ZenossSecurity import ZEN_CHANGE_DEVICE +from Products.ZenRelations.RelSchema import ToManyCont, ToOne + + +class ExampleComponent(DeviceComponent, ManagedEntity): + meta_type = portal_type = "ExampleComponent" + + attributeOne = None + attributeTwo = None + + _properties = ManagedEntity._properties + ( + {'id': 'attributeOne', 'type': 'int', 'mode': ''}, + {'id': 'attributeTwo', 'type': 'string', 'mode': ''}, + ) + + _relations = ManagedEntity._relations + ( + ('exampleDevice', ToOne(ToManyCont, + 'ZenPacks.NAMESPACE.PACKNAME.ExampleDevice.ExampleDevice', + 'exampleComponents', + ), + ), + ) + + # Defining the "perfConf" action here causes the "Graphs" display to be + # available for components of this type. + factory_type_information = ({ + 'actions': ({ + 'id': 'perfConf', + 'name': 'Template', + 'action': 'objTemplates', + 'permissions': (ZEN_CHANGE_DEVICE,), + },), + },) + + # Custom components must always implement the device method. The method + # should return the device object that contains the component. + def device(self): + return self.exampleDevice() diff --git a/CONTENT/ExampleDevice.py b/CONTENT/ExampleDevice.py new file mode 100644 index 0000000..a317f17 --- /dev/null +++ b/CONTENT/ExampleDevice.py @@ -0,0 +1,28 @@ +from Products.ZenModel.Device import Device +from Products.ZenRelations.RelSchema import ToManyCont, ToOne + + +class ExampleDevice(Device): + """ + Example device subclass. In this case the reason for creating a subclass of + device is to add a new type of relation. We want many "ExampleComponent" + components to be associated with each of these devices. + + If you set the zPythonClass of a device class to + ZenPacks.NAMESPACE.PACKNAME.ExampleDevice, any devices created or moved + into that device class will become this class and be able to contain + ExampleComponents. + """ + + meta_type = portal_type = 'ExampleDevice' + + # This is where we extend the standard relationships of a device to add + # our "exampleComponents" relationship that must be filled with components + # of our custom "ExampleComponent" class. + _relations = Device._relations + ( + ('exampleComponents', ToManyCont(ToOne, + 'ZenPacks.NAMESPACE.PACKNAME.ExampleComponent.ExampleComponent', + 'exampleDevice', + ), + ), + ) diff --git a/CONTENT/__init__.py b/CONTENT/__init__.py new file mode 100644 index 0000000..e1d3c12 --- /dev/null +++ b/CONTENT/__init__.py @@ -0,0 +1,47 @@ +# Nothing is required in this __init__.py, but it is an excellent place to do +# many things in a ZenPack. +# +# The example below which is commented out by default creates a custom subclass +# of the ZenPack class. This allows you to define custom installation and +# removal routines for your ZenPack. If you don't need this kind of flexibility +# you should leave the section commented out and let the standard ZenPack +# class be used. +# +# Code included in the global scope of this file will be executed at startup +# in any Zope client. This includes Zope itself (the web interface) and zenhub. +# This makes this the perfect place to alter lower-level stock behavior +# through monkey-patching. + +# import Globals +# +# from Products.ZenModel.ZenPack import ZenPack as ZenPackBase +# from Products.ZenUtils.Utils import unused +# +# unused(Globals) +# +# +# class ZenPack(ZenPackBase): +# +# # All zProperties defined here will automatically be created when the +# # ZenPack is installed. +# packZProperties = [ +# ('zExampleString', 'default value', 'string'), +# ('zExampleInt', 411, 'int'), +# ('zExamplePassword', 'notsecure', 'password'), +# ] +# +# def install(self, dmd): +# ZenPackBase.install(self, dmd) +# +# # Put your customer installation logic here. +# pass +# +# def remove(self, dmd, leaveObjects=False): +# if not leaveObjects: +# # When a ZenPack is removed the remove method will be called with +# # leaveObjects set to False. This means that you likely want to +# # make sure that leaveObjects is set to false before executing +# # your custom removal code. +# pass +# +# ZenPackBase.remove(self, dmd, leaveObjects=leaveObjects) diff --git a/CONTENT/analytics.py b/CONTENT/analytics.py new file mode 100644 index 0000000..e086c09 --- /dev/null +++ b/CONTENT/analytics.py @@ -0,0 +1,31 @@ +from zope.component import adapts +from zope.interface import implements + +from Products.Zuul.interfaces import IReportable + +from ZenPacks.zenoss.ZenETL.reportable \ + import Reportable, MARKER_LENGTH, DEFAULT_STRING_LENGTH + +from .ExampleComponent import ExampleComponent + + +class ExampleComponentReportable(Reportable): + implements(IReportable) + adapts(ExampleComponent) + + @property + def entity_class_name(self): + return 'example_component' + + def reportProperties(self): + """ + We want to export our two custom properties to the data warehouse for + reporting. + """ + return [ + ('attributeOne', 'int', + self.context.attribuetOne, MARKER_LENGTH), + + ('attributeTwo', 'string', + self.context.attributeTwo, DEFAULT_STRING_LENGTH), + ] diff --git a/CONTENT/bin/placeholder.txt b/CONTENT/bin/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/browser/__init__.py b/CONTENT/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/browser/configure.zcml b/CONTENT/browser/configure.zcml new file mode 100644 index 0000000..616befb --- /dev/null +++ b/CONTENT/browser/configure.zcml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/CONTENT/browser/resources/css/placeholder.txt b/CONTENT/browser/resources/css/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/browser/resources/img/placeholder.txt b/CONTENT/browser/resources/img/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/browser/resources/js/ExampleDevice.js b/CONTENT/browser/resources/js/ExampleDevice.js new file mode 100644 index 0000000..fffd1a8 --- /dev/null +++ b/CONTENT/browser/resources/js/ExampleDevice.js @@ -0,0 +1,81 @@ +/* + * Based on the configuration in ../../configure.zcml this JavaScript will only + * be loaded when the user is looking at an ExampleDevice in the web interface. + */ + +(function(){ + +var ZC = Ext.ns('Zenoss.component'); + + +/* + * Friendly names for the components. First parameter is the meta_type in your + * custom component class. Second parameter is the singular form of the + * friendly name to be displayed in the UI. Third parameter is the plural form. + */ +ZC.registerName('ExampleComponent', _t('Example'), _t('Examples')); + + +/* + * Custom component grid panel. This controls the grid that gets displayed for + * components of the type set in "componenType". + */ +ZC.ExampleComponentGridPanel = Ext.extend(ZC.ComponentGridPanel, { + subComponentGridPanel: false, + + constructor: function(config) { + config = Ext.applyIf(config||{}, { + autoExpandColumn: 'name', + componentType: 'ExampleComponent', + sortInfo: { + field: 'name', + direction: 'ASC' + }, + fields: [ + {name: 'uid'}, + {name: 'name'}, + {name: 'severity'}, + {name: 'attributeOne'}, + {name: 'attributeTwo'}, + {name: 'monitor'}, + {name: 'monitored'} + ], + columns: [{ + id: 'severity', + dataIndex: 'severity', + header: _t('Events'), + renderer: Zenoss.render.severity, + sortable: true, + width: 50 + },{ + id: 'name', + dataIndex: 'name', + header: _t('Name') + },{ + id: 'attributeOne', + dataIndex: 'attributeOne', + header: _t('Attribute #1'), + sortable: true, + width: 70 + },{ + id: 'attributeTwo', + dataIndex: 'attributeTwo', + header: _t('Attribute #2'), + sortable: true, + width: 70 + },{ + id: 'monitored', + dataIndex: 'monitored', + header: _t('Monitored'), + renderer: Zenoss.render.checkbox, + sortable: true, + width: 65 + }] + }); + ZC.ExampleComponentGridPanel.superclass.constructor.call(this, config); + } +}); + +Ext.reg('ExampleComponentGridPanel', ZC.ExampleComponentGridPanel); + +})(); diff --git a/CONTENT/configure.zcml b/CONTENT/configure.zcml new file mode 100644 index 0000000..70c1478 --- /dev/null +++ b/CONTENT/configure.zcml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CONTENT/daemons/zenexample b/CONTENT/daemons/zenexample new file mode 100644 index 0000000..8fb65ba --- /dev/null +++ b/CONTENT/daemons/zenexample @@ -0,0 +1,16 @@ +#! /usr/bin/env bash + +# You must change the name of your daemon to something other than zenexample. +# The "zenexample" name is special and will be ignored by the ZenPack +# installation routine. +DAEMON_NAME="zenexample" + +. $ZENHOME/bin/zenfunctions + +MYPATH=`python -c "import os.path; print os.path.realpath('$0')"` +THISDIR=`dirname $MYPATH` +PRGHOME=`dirname $THISDIR` +PRGNAME=$DAEMON_NAME.py +CFGFILE=$CFGDIR/$DAEMON_NAME.conf + +generic "$@" diff --git a/CONTENT/datasources/ExampleDataSource.py.example b/CONTENT/datasources/ExampleDataSource.py.example new file mode 100644 index 0000000..63ef199 --- /dev/null +++ b/CONTENT/datasources/ExampleDataSource.py.example @@ -0,0 +1,37 @@ +# Creating a custom datasource type requires defining a subclass of +# Products.ZenModel.RRDDataSource.RRDDataSource as illustrated below. This +# class gets instantiated, configured and stored in the ZODB everytime someone +# adds this type of datasource to a template. + +# You will also need to add an IRRDDataSourceInfo subinterface to control how +# the user interface for configuring this datasource is drawn. This interface +# is typically defined in ../interfaces.py. You will then need to define a +# RRDDataSourceInfo subclass to control how your datasource gets serialized +# for passing through the API. This info adapter class is typically defined in +# ../info.py. + +from Products.ZenModel.RRDDataSource import RRDDataSource +from Products.ZenModel.ZenPackPersistence import ZenPackPersistence + + +class ExampleDataSource(ZenPackPersistence, RRDDataSource): + + # All subclasses of ZenPackPersistence need to set their ZENPACKID. + ZENPACKID = 'ZenPacks.NAMESPACE.PACKNAME' + + # These define how this datasource type is displayed in the datasource type + # picker when adding a new datasource to a monitoring template. Keep it + # short and unambiguous. + sourcetypes = ('Example Protocol',) + sourcetype = sourcetypes[0] + + # Set default values for properties inherited from RRDDataSource. + eventClass = '/Status/Example' + component = "${here/id}" + + # Add default values for custom properties of this datasource. + exampleProperty = '' + + _properties = RRDDataSource._properties + ( + {'id': 'exampleProperty', 'type': 'string'}, + ) diff --git a/CONTENT/datasources/__init__.py b/CONTENT/datasources/__init__.py new file mode 100644 index 0000000..651585a --- /dev/null +++ b/CONTENT/datasources/__init__.py @@ -0,0 +1,2 @@ +# __init__.py + diff --git a/CONTENT/dynamicview.py b/CONTENT/dynamicview.py new file mode 100644 index 0000000..44aae1d --- /dev/null +++ b/CONTENT/dynamicview.py @@ -0,0 +1,42 @@ +from zope.component import adapts + +from ZenPacks.zenoss.DynamicView import TAG_IMPACTED_BY, TAG_IMPACTS, TAG_ALL +from ZenPacks.zenoss.DynamicView.model.adapters import DeviceComponentRelatable +from ZenPacks.zenoss.DynamicView.model.adapters import BaseRelationsProvider + +from ..ExampleDevice import ExampleDevice +from ..ExampleComponent import ExampleComponent + + +### IRelatable Adapters + +class ExampleComponentRelatable(DeviceComponentRelatable): + adapts(ExampleComponent) + + group = 'Example Components' + + +### IRelationsProvider Adapters + +class ExampleDeviceRelationsProvider(BaseRelationsProvider): + adapts(ExampleDevice) + + def relations(self, type=TAG_ALL): + """ + ExampleDevices impact all of their ExampleComponents. + """ + if type in (TAG_ALL, TAG_IMPACTS): + for exampleComponent in self._adapted.exampleComponents(): + yield self.constructRelationTo(exampleComponent, TAG_IMPACTS) + + +class ExampleComponentRelationsProvider(BaseRelationsProvider): + adapts(ExampleComponent) + + def relations(self, type=TAG_ALL): + """ + ExampleComponents are impacted by their ExampleDevice. + """ + if type in (TAG_ALL, TAG_IMPACTED_BY): + yield self.constructRelationTo( + self._adapted.exampleDevice(), TAG_IMPACTED_BY) diff --git a/CONTENT/events.py b/CONTENT/events.py new file mode 100644 index 0000000..7db288c --- /dev/null +++ b/CONTENT/events.py @@ -0,0 +1,16 @@ +class ExamplePreEventPlugin(object): + def apply(self, eventProxy, dmd): + event = eventProxy._zepRawEvent.event + + # Do something to the event. Any changes made to the event object will + # be saved to it. You should not return anything from this method. + event.summary = 'ExamplePreEventPlugin changed the summary' + + +class ExamplePostEventPlugin(object): + def apply(self, eventProxy, dmd): + event = eventProxy._zepRawEvent.event + + # Do something to the event. Any changes made to the event object will + # be saved to it. You should not return anything from this method. + event.summary = 'ExamplePostEventPlugin changed the summary' diff --git a/CONTENT/impact.py b/CONTENT/impact.py new file mode 100644 index 0000000..632881d --- /dev/null +++ b/CONTENT/impact.py @@ -0,0 +1,132 @@ +from zope.component import adapts +from zope.interface import implements + +from Products.ZenUtils.guid.interfaces import IGlobalIdentifier + +from ZenPacks.zenoss.Impact.impactd import Trigger +from ZenPacks.zenoss.Impact.stated.interfaces import IStateProvider +from ZenPacks.zenoss.Impact.impactd.relations import ImpactEdge +from ZenPacks.zenoss.Impact.impactd.interfaces \ + import IRelationshipDataProvider, INodeTriggers + +from .ExampleDevice import ExampleDevice +from .ExampleComponent import ExampleComponent + + +def getRedundancyTriggers(guid, format): + """ + Helper method for generating a good general redunancy set of triggers. + """ + + availability = 'AVAILABILITY' + percent = 'policyPercentageTrigger' + threshold = 'policyThresholdTrigger' + + return ( + Trigger(guid, format % 'DOWN', percent, availability, dict( + state='DOWN', dependentState='DOWN', threshold='100', + )), + Trigger(guid, format % 'DEGRADED', threshold, availability, dict( + state='DEGRADED', dependentState='DEGRADED', threshold='1', + )), + Trigger(guid, format % 'ATRISK_1', threshold, availability, dict( + state='ATRISK', dependentState='DOWN', threshold='1', + )), + Trigger(guid, format % 'ATRISK_2', threshold, availability, dict( + state='ATRISK', dependentState='ATRISK', threshold='1', + )), + ) + + +class ExampleDeviceRelationsProvider(object): + implements(IRelationshipDataProvider) + adapts(ExampleDevice) + + relationship_provider = "ExampleImpact" + + def __init__(self, adapted): + self._object = adapted + + def belongsInImpactGraph(self): + return True + + def getEdges(self): + """ + An ExampleDevice impacts all of its ExampleComponents. + """ + guid = IGlobalIdentifier(self._object).getGUID() + + for exampleComponent in self._object.exampleComponents(): + c_guid = IGlobalIdentifier(exampleComponent).getGUID() + yield ImpactEdge(guid, c_guid, self.relationship_provider) + + +class ExampleComponentRelationsProvider(object): + implements(IRelationshipDataProvider) + adapts(ExampleComponent) + + relationship_provider = "ExampleImpact" + + def __init__(self, adapted): + self._object = adapted + + def belongsInImpactGraph(self): + return True + + def getEdges(self): + """ + An ExampleComponent is impacted by its ExampleDevice. + """ + guid = IGlobalIdentifier(self._object).getGUID() + + d_guid = IGlobalIdentifier(self._object.exampleDevice()) + yield ImpactEdge(d_guid, guid, self.relationship_provider) + + +class ExampleComponentStateProvider(object): + implements(IStateProvider) + + def __init__(self, adapted): + self._object = adapted + + @property + def eventClasses(self): + return ('/Status/',) + + @property + def excludeClasses(self): + return None + + @property + def eventHandlerType(self): + return "WORST" + + @property + def stateType(self): + return 'AVAILABILITY' + + def calcState(self, events): + status = None + if self._object.attributeOne < 1: + return 'DOWN' + else: + return 'UP' + + cause = None + if status == 'DOWN' and events: + cause = events[0] + + return status, cause + + +class ExampleComponentTriggers(object): + implements(INodeTriggers) + + def __init__(self, adapted): + self._object = adapted + + def get_triggers(self): + return getRedundancyTriggers( + IGlobalIdentifier(self._object).getGUID(), + 'DEFAULT_EXAMPLECOMPONENT_TRIGGER_ID_%s', + ) diff --git a/CONTENT/info.py b/CONTENT/info.py new file mode 100644 index 0000000..46906bb --- /dev/null +++ b/CONTENT/info.py @@ -0,0 +1,53 @@ +# This file is the conventional place for "Info" adapters. Info adapters are +# a crucial part of the Zenoss API and therefore the web interface for any +# custom classes delivered by your ZenPack. Examples of custom classes that +# will almost certainly need info adapters include datasources, custom device +# classes and custom device component classes. + +# Mappings of interfaces (interfaces.py) to concrete classes and the factory +# (these info adapter classes) used to create info objects for them are managed +# in the configure.zcml file. + +from zope.component import adapts +from zope.interface import implements + +from Products.Zuul.infos import ProxyProperty +from Products.Zuul.infos.component import ComponentInfo +from Products.Zuul.infos.template import RRDDataSourceInfo + +from ZenPacks.NAMESPACE.PACKNAME.ExampleComponent import ExampleComponent +from ZenPacks.NAMESPACE.PACKNAME.interfaces \ + import IExampleDataSourceInfo, IExampleComponentInfo + + +class ExampleDataSourceInfo(RRDDataSourceInfo): + """ + Defines API access for this datasource. + """ + + implements(IExampleDataSourceInfo) + + # ProxyProperty is a shortcut to mean that you want the getter/setter for + # this property to go directly to properties of the same name on the + # datasource class (ExampleDataSource). + exampleProperty = ProxyProperty('exampleProperty') + + # RRDDataSourceInfo classes can create a property called "testable" that + # controls whether the datasource dialog in the web interface allows the + # user to test it. By default this property is set to True unless you + # override it as is done below. + + @property + def testable(self): + """ + This datasource is not testable. + """ + return False + + +class ExampleComponentInfo(ComponentInfo): + implements(IExampleComponentInfo) + adapts(ExampleComponent) + + attributeOne = ProxyProperty("attributeOne") + attributeTwo = ProxyProperty("attributeTwo") diff --git a/CONTENT/interfaces.py b/CONTENT/interfaces.py new file mode 100644 index 0000000..dbc566e --- /dev/null +++ b/CONTENT/interfaces.py @@ -0,0 +1,45 @@ +from Products.Zuul.form import schema +from Products.Zuul.interfaces.component import IComponentInfo +from Products.Zuul.interfaces.template import IRRDDataSourceInfo + +# ZuulMessageFactory is the translation layer. You will see strings intended to +# been seen in the web interface wrapped in _t(). This is so that these strings +# can be automatically translated to other languages. +from Products.Zuul.utils import ZuulMessageFactory as _t + +# In Zenoss 3 we mistakenly mapped TextLine to Zope's multi-line text +# equivalent and Text to Zope's single-line text equivalent. This was +# backwards so we flipped their meanings in Zenoss 4. The following block of +# code allows the ZenPack to work properly in Zenoss 3 and 4. + +# Until backwards compatibility with Zenoss 3 is no longer desired for your +# ZenPack it is recommended that you use "SingleLineText" and "MultiLineText" +# instead of schema.TextLine or schema.Text. +from Products.ZenModel.ZVersion import VERSION as ZENOSS_VERSION +from Products.ZenUtils.Version import Version +if Version.parse('Zenoss %s' % ZENOSS_VERSION) >= Version.parse('Zenoss 4'): + SingleLineText = schema.TextLine + MultiLineText = schema.Text +else: + SingleLineText = schema.Text + MultiLineText = schema.TextLine + + +class IExampleDataSourceInfo(IRRDDataSourceInfo): + """ + Defines what fields should be displayed on the edit dialog for this + datasource in the Zenoss web interface. + """ + + # We inherit common datasource fields like event class, severity and others + # from IRRDDataSourceInfo. + + exampleProperty = SingleLineText( + title=_t(u'Example Property'), + group=_t(u'Example Protocol'), + ) + + +class IExampleComponentInfo(IComponentInfo): + attributeOne = schema.Int(title=_t(u"Attribute #1")) + attributeTwo = SingleLineText(title=_t(u"Attribute #2")) diff --git a/CONTENT/lib/__init__.py b/CONTENT/lib/__init__.py new file mode 100644 index 0000000..143f486 --- /dev/null +++ b/CONTENT/lib/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/CONTENT/libexec/placeholder.txt b/CONTENT/libexec/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/migrate/ExampleMigration.py b/CONTENT/migrate/ExampleMigration.py new file mode 100644 index 0000000..1ac270b --- /dev/null +++ b/CONTENT/migrate/ExampleMigration.py @@ -0,0 +1,34 @@ +import logging +log = logging.getLogger('zen.migrate') + +import Globals + +from Products.ZenModel.ZenPack import ZenPackMigration +from Products.ZenModel.migrate.Migrate import Version +from Products.ZenUtils.Utils import unused + +unused(Globals) + + +# Your migration class must subclass ZenPackMigration. +class ExampleMigration(ZenPackMigration): + + # There are two scenarios under which this migrate script will execute. + # 1. Fresh Install - If this ZenPack is being installed for the first + # time and the migrate script version is greater than or equal to the + # ZenPack's version, it will execute. + # + # 2. Upgrade - If this ZenPack is being upgraded and the migrate script + # version is greater than or equal to the version of the ZenPack that + # is already installed, it will execute. + version = Version(0, 0, 1) + + def migrate(self, dmd): + log.info("Running ExampleMigration") + + # Do the migration work. No commit is needed. + pass + + +# Run the migration when this file is imported. +ExampleMigration() diff --git a/CONTENT/migrate/__init__.py b/CONTENT/migrate/__init__.py new file mode 100644 index 0000000..143f486 --- /dev/null +++ b/CONTENT/migrate/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/CONTENT/modeler/__init__.py b/CONTENT/modeler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/modeler/plugins/__init__.py b/CONTENT/modeler/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/modeler/plugins/community/__init__.py b/CONTENT/modeler/plugins/community/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/modeler/plugins/community/cmd/ExampleCMD.py.example b/CONTENT/modeler/plugins/community/cmd/ExampleCMD.py.example new file mode 100644 index 0000000..c1c99c1 --- /dev/null +++ b/CONTENT/modeler/plugins/community/cmd/ExampleCMD.py.example @@ -0,0 +1,81 @@ +# Module-level documentation will automatically be shown as additional +# information for the modeler plugin in the web interface. +""" +ExampleCMD +An example plugin that illustrates how to model devices using SSH. +""" + +# This is an example of an CMD-based modeler plugin. It won't be recognized by +# Zenoss as an available modeler plugin unless the .example extension is +# removed. + +# When configuring modeler plugins for a device or device class, this plugin's +# name would be community.snmp.ExampleCMD because its filesystem path within +# the ZenPack is modeler/plugins/community/snmp/ExampleCMD.py. The name of the +# class within this file must match the filename. + +import re + +# CommandPlugin is the base class that provides lots of help in modeling data +# that's available by connecting to a remote machine, running command line +# tools, and parsing their results. +from Products.DataCollector.plugins.CollectorPlugin import CommandPlugin + +# Classes we'll need for returning proper results from our modeler plugin's +# process method. +from Products.DataCollector.plugins.DataMaps import ObjectMap, RelationshipMap + + +class ExampleCMD(CommandPlugin): + + # The command to run. + command = "/bin/cat /proc/partitions" + + # Modeler plugins can optionally implement the "condition" method. This + # allows your plugin to determine if it should be run by looking at the + # configuration of the device that's about to be modeled. Return True if + # you want the modeler plugin to execute and False if you do not. + # + # The default is to return True. So ordinarily you wouldn't even implement + # the method if you were just going to blindly return True like this + # example. + def condition(self, device, log): + return True + + def process(self, device, results, log): + log.info("Modeler %s processing data for device %s", + self.name(), device.id) + + objectmaps = [] + + # For CommandPlugin, the results parameter to the process method will + # be a string containing all output from the command defined above. + + # results contents.. + # major minor #blocks name + # + # 8 0 41943040 sda + # 8 1 104391 sda1 + # 8 2 41833260 sda2 + # 253 0 41091072 dm-0 + # 253 1 720896 dm-1 + + matcher = re.compile(r'^\d+\s+\d+\s+(?P\d+)\s+(?P\S+)') + + for line in results.split('\n'): + line = line.strip() + match = matcher.search(line) + if match: + objectmaps.append(ObjectMap({ + 'id': self.prepId(match.group('name')), + 'description': match.group('name'), + })) + + # Return a RelationshipMap that describes the component, relationship + # on that component, and the module name for the created objects. Pass + # in the previously built list of ObjectMaps that will be used to + # populate the relationship. + return RelationshipMap( + compname="hw", relname="harddisks", + modname='Products.ZenModel.HardDisk', + objmaps=objectmaps) diff --git a/CONTENT/modeler/plugins/community/cmd/__init__.py b/CONTENT/modeler/plugins/community/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/modeler/plugins/community/snmp/ExampleSNMP.py.example b/CONTENT/modeler/plugins/community/snmp/ExampleSNMP.py.example new file mode 100644 index 0000000..3d8fa51 --- /dev/null +++ b/CONTENT/modeler/plugins/community/snmp/ExampleSNMP.py.example @@ -0,0 +1,121 @@ +# Module-level documentation will automatically be shown as additional +# information for the modeler plugin in the web interface. +""" +ExampleSNMP +An example plugin that illustrates how to model devices using SNMP. +""" + +# This is an example of an SNMP-based modeler plugin. It won't be recognized by +# Zenoss as an available modeler plugin unless the .example extension is +# removed. + +# When configuring modeler plugins for a device or device class, this plugin's +# name would be community.snmp.ExampleSNMP because its filesystem path within +# the ZenPack is modeler/plugins/community/snmp/ExampleSNMP.py. The name of the +# class within this file must match the filename. + +# SnmpPlugin is the base class that provides lots of help in modeling data +# that's available over SNMP. +from Products.DataCollector.plugins.CollectorPlugin \ + import SnmpPlugin, GetMap, GetTableMap + +# Classes we'll need for returning proper results from our modeler plugin's +# process method. +from Products.DataCollector.plugins.DataMaps import ObjectMap + + +class ExampleSNMP(SnmpPlugin): + + # SnmpPlugin will automatically collect OIDs described in the snmpGetMap + # property. You can make up the value for the OID key. It will be used in + # the process method to find the result for each value. snmpGetMap and + # GetMap should be used to request specific OIDs as you would in an + # snmpget. + snmpGetMap = GetMap({ + '.1.3.6.1.4.1.2021.4.3.0': 'memTotalSwap', + '.1.3.6.1.4.1.2021.4.5.0': 'memTotalReal', + }) + + # snmpGetTableMaps and GetTableMap should be used to request SNMP tables. + # The first parameter to GetTableMap is whatever you want the results of + # this table to be stored in the results as. The second parameter is the + # base OID for the table. More specifically this should be the "entry" OID + # or more specifically the largest possible OID prefix that doesn't change + # when walking the table. The third paramter is a dictionary that maps + # columns in the table to names that will be used to access them in the + # results. + snmpGetTableMaps = ( + GetTableMap('diskIOTable', '.1.3.6.1.4.1.2021.13.15.1.1', { + '.1': 'index', + '.2': 'device', + }), + + # More GetTableMap definitions can be added to this tuple to query + # more SNMP tables. + ) + + # Modeler plugins can optionally implement the "condition" method. This + # allows your plugin to determine if it should be run by looking at the + # configuration of the device that's about to be modeled. Return True if + # you want the modeler plugin to execute and False if you do not. + # + # The default is to return True. So ordinarily you wouldn't even implement + # the method if you were just going to blindly return True like this + # example. + def condition(self, device, log): + return True + + def process(self, device, results, log): + log.info("Modeler %s processing data for device %s", + self.name(), device.id) + + # Results is a tuple with two items. The first (0) index contains a + # dictionary with the results of our "snmpGetMap" queries. The second + # (1) index contains a dictionary with the results of our + # "snmpGetTableMaps" queries. + getdata, tabledata = results + + # getdata contents.. + # {'memTotalReal': 2058776, 'memTotalSwap': 720888} + + # tabledata contents.. + # {'diskIOTable': {'1': {'device': 'ram0', 'index': 1}, + # '2': {'device': 'ram1', 'index': 2}, + # '3': {'device': 'ram2', 'index': 3}, + # '4': {'device': 'ram4', 'index': 4}}} + + # Create a list to fill up with our results. + maps = [] + + # First we build an ObjectMap to apply to the device's hardware (hw) + # component to set the total memory size. Multiple the returned value + # by 1024 because the SNMP result is in kilybytes and we want to store + # it in bytes. + maps.append(ObjectMap({ + 'totalMemory': getdata['memTotalReal'] * 1024}, + compname='hw')) + + # Now do the same thing for total swap space. Zenoss stores this on the + # Operating System (os) component of the device. + maps.append(ObjectMap({ + 'totalSwap': getdata['memTotalSwap'] * 1024}, + compname='os')) + + # Log for each disk returned from our GetTableMap. If we wanted to + # create new disks in the model we'd create a RelationshipMap for them + # and add an ObjectMap to it for each row in this table. See the + # ExampleCMD plugin for an example of this. + for snmpindex, disk in tabledata.get('diskIOTable').items(): + log.info("Found disk %s", disk['device']) + + # The process method of the modeler plugin class below is expected to + # return output in one of the following forms. + # + # 1. A single ObjectMap instance + # 2. A single RelationshipMap instance + # 3. A list of ObjectMap and RelationshipMap instances + # 4. None + # + # If your modeler plugin encounters a bad state and you don't want to + # affect Zenoss' model of the device you should return None. + return maps diff --git a/CONTENT/modeler/plugins/community/snmp/__init__.py b/CONTENT/modeler/plugins/community/snmp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/objects/objects.xml b/CONTENT/objects/objects.xml new file mode 100644 index 0000000..5908b64 --- /dev/null +++ b/CONTENT/objects/objects.xml @@ -0,0 +1,3 @@ + + + diff --git a/CONTENT/reports/Example Reports/Example Report.rpt.example b/CONTENT/reports/Example Reports/Example Report.rpt.example new file mode 100644 index 0000000..08b48c1 --- /dev/null +++ b/CONTENT/reports/Example Reports/Example Report.rpt.example @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + diff --git a/CONTENT/reports/plugins/example_plugin.py b/CONTENT/reports/plugins/example_plugin.py new file mode 100644 index 0000000..bc29956 --- /dev/null +++ b/CONTENT/reports/plugins/example_plugin.py @@ -0,0 +1,23 @@ +# ZenReports.Utils contains some useful helpers for creating records to return. +from Products.ZenReports import Utils + + +# The class name must patch the filename. +class example_plugin: + + # The run method will be executed when your report calls the plugin. + def run(self, dmd, args): + report = [] + for device in dmd.Devices.getSubDevicesGen(): + report.append(Utils.Record( + device=device.titleOrId(), + ip=device.manageIp, + hardware="%s %s" % ( + device.hw.getManufacturerName(), + device.hw.getProductName()), + software="%s %s" % ( + device.os.getManufacturerName(), + device.os.getProductName()), + )) + + return report diff --git a/CONTENT/services/ExampleConfigService.py b/CONTENT/services/ExampleConfigService.py new file mode 100644 index 0000000..33955d8 --- /dev/null +++ b/CONTENT/services/ExampleConfigService.py @@ -0,0 +1,94 @@ +""" +ExampleConfigService +ZenHub service for providing configuration to the zenexample collector daemon. + + This provides the daemon with a dictionary of datapoints for every device. +""" + +import logging +log = logging.getLogger('zen.example') + +from Products.ZenCollector.services.config import CollectorConfigService + + +# Your daemon configuration service should almost certainly subclass +# CollectorConfigService to make it as easy as possible for you to implement. +class ExampleConfigService(CollectorConfigService): + """ + ZenHub service for the zenexample collector daemon. + """ + + # When the collector daemon requests a list of devices to poll from ZenHub + # your service can filter the devices that are returned by implementing + # this _filterDevice method. If _filterDevice returns True for a device, + # it will be returned to the collector. If _filterDevice returns False, the + # collector daemon won't collect from it. + def _filterDevice(self, device): + # First use standard filtering. + filter = CollectorConfigService._filterDevice(self, device) + + # If the standard filtering logic said the device shouldn't be filtered + # we can setup some other contraint. + if filter: + # We only monitor devices that start with "z". + return device.id.startswith('z') + + return filter + + # The _createDeviceProxy method allows you to build up the DeviceProxy + # object that will be sent to the collector daemon. Whatever is returned + # from this method will be sent as the device's representation to the + # collector daemon. Use serializable types. DeviceProxy works, as do any + # simple Python types. + def _createDeviceProxy(self, device): + proxy = CollectorConfigService._createDeviceProxy(self, device) + + proxy.datapoints = [] + proxy.thresholds = [] + + perfServer = device.getPerformanceServer() + + self._getDataPoints(proxy, device, device.id, None, perfServer) + proxy.thresholds += device.getThresholdInstances('Example Protocol') + + for component in device.getMonitoredComponents(): + self._getDataPoints( + proxy, component, component.device().id, component.id, + perfServer) + + proxy.thresholds += component.getThresholdInstances( + 'Example Protocol') + + return proxy + + # This is not a method you must implement. It is used by the custom + # _createDeviceProxy method above. + def _getDataPoints( + self, proxy, deviceOrComponent, deviceId, componentId, perfServer + ): + for template in deviceOrComponent.getRRDTemplates(): + dataSources = [ds for ds + in template.getRRDDataSources('Example Protocol') + if ds.enabled] + + for ds in dataSources: + for dp in ds.datapoints(): + path = '/'.join((deviceOrComponent.rrdPath(), dp.name())) + dpInfo = dict( + devId=deviceId, + compId=componentId, + dsId=ds.id, + dpId=dp.id, + path=path, + rrdType=dp.rrdtype, + rrdCmd=dp.getRRDCreateCommand(perfServer), + minv=dp.rrdmin, + maxv=dp.rrdmax, + exampleProperty=ds.exampleProperty, + ) + + if componentId: + dpInfo['componentDn'] = getattr( + deviceOrComponent, 'dn', None) + + proxy.datapoints.append(dpInfo) diff --git a/CONTENT/services/__init__.py b/CONTENT/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/tests/__init__.py b/CONTENT/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CONTENT/tests/testExample.py b/CONTENT/tests/testExample.py new file mode 100644 index 0000000..8e0f265 --- /dev/null +++ b/CONTENT/tests/testExample.py @@ -0,0 +1,36 @@ +# BaseTestCase is a subclass of ZopeTestCase which is ultimately a subclass of +# Python's standard unittest.TestCase. Because of this the following +# documentation on unit testing in Zope and Python are both applicable here. +# +# Python Unit testing framework +# http://docs.python.org/library/unittest.html +# +# Zope Unit Testing +# http://wiki.zope.org/zope2/Testing + +from Products.ZenTestCase.BaseTestCase import BaseTestCase + + +class TestExample(BaseTestCase): + def afterSetup(self): + # You can use the afterSetup method to create a proper environment for + # your tests to execute in, or to run common code between the tests. + self.device = self.dmd.Devices.createInstance('testDevice') + + def testExampleOne(self): + self.assertEqual("One", "One") + self.assertTrue(True) + + def testExampleTwo(self): + self.assertEqual(self.device.id, "testDevice") + self.assertFalse(False) + + +def test_suite(): + from unittest import TestSuite, makeSuite + suite = TestSuite() + + # Add your BaseTestCase subclasses here to have them executed. + # suite.addTest(makeSuite(TestExample)) + + return suite diff --git a/CONTENT/zenexample.py b/CONTENT/zenexample.py new file mode 100644 index 0000000..1eabbef --- /dev/null +++ b/CONTENT/zenexample.py @@ -0,0 +1,108 @@ +# This is an example of a custom collector daemon. + +import logging +log = logging.getLogger('zen.Example') + +import Globals +import zope.component +import zope.interface + +from twisted.internet import defer + +from Products.ZenCollector.daemon import CollectorDaemon +from Products.ZenCollector.interfaces \ + import ICollectorPreferences, IScheduledTask, IEventService, IDataService + +from Products.ZenCollector.tasks \ + import SimpleTaskFactory, SimpleTaskSplitter, TaskStates + +from Products.ZenUtils.observable import ObservableMixin + +# unused is way to keep Python linters from complaining about imports that we +# don't explicitely use. Occasionally there is a valid reason to do this. +from Products.ZenUtils.Utils import unused + +# We must import our ConfigService here so zenhub will allow it to be +# serialized and deserialized. We'll declare it unused to satisfy linters. +from ZenPacks.NAMESPACE.PACKNAME.services.ExampleConfigService \ + import ExampleConfigService + +unused(Globals) +unused(ExampleConfigService) + + +# Your implementation of ICollectorPreferences is where you can handle custom +# command line (or config file) options and do global configuration of the +# daemon. +class ZenExamplePreferences(object): + zope.interface.implements(ICollectorPreferences) + + def __init__(self): + self.collectorName = 'zenexample' + self.configurationService = \ + "ZenPacks.NAMESPACE.PACKNAME.services.ExampleConfigService" + + # How often the daemon will collect each device. Specified in seconds. + self.cycleInterval = 5 * 60 + + # How often the daemon will reload configuration. In seconds. + self.configCycleInterval = 5 * 60 + + self.options = None + + def buildOptions(self, parser): + """ + Required to implement the ICollectorPreferences interface. + """ + pass + + def postStartup(self): + """ + Required to implement the ICollectorPreferences interface. + """ + pass + + +# The implementation of IScheduledTask for your daemon is usually where most +# of the work is done. This is where you implement the specific logic required +# to collect data. +class ZenExampleTask(ObservableMixin): + zope.interface.implements(IScheduledTask) + + def __init__(self, taskName, deviceId, interval, taskConfig): + super(ZenExampleTask, self).__init__() + self._taskConfig = taskConfig + + self._eventService = zope.component.queryUtility(IEventService) + self._dataService = zope.component.queryUtility(IDataService) + self._preferences = zope.component.queryUtility( + ICollectorPreferences, 'zenexample') + + # All of these properties are required to implement the IScheduledTask + # interface. + self.name = taskName + self.configId = deviceId + self.interval = interval + self.state = TaskStates.STATE_IDLE + + # doTask is where the collector logic should go. It is also required to + # implement the IScheduledTask interface. It will be called directly by the + # framework when it's this task's turn to run. + def doTask(self): + # This method must return a deferred because the collector framework + # is asynchronous. + d = defer.Deferred() + return d + + # cleanup is required to implement the IScheduledTask interface. + def cleanup(self): + pass + + +if __name__ == '__main__': + myPreferences = ZenExamplePreferences() + myTaskFactory = SimpleTaskFactory(ZenExampleTask) + myTaskSplitter = SimpleTaskSplitter(myTaskFactory) + + daemon = CollectorDaemon(myPreferences, myTaskSplitter) + daemon.run() diff --git a/CONTENT/zep/actions.json.example b/CONTENT/zep/actions.json.example new file mode 100644 index 0000000..b7d6d9d --- /dev/null +++ b/CONTENT/zep/actions.json.example @@ -0,0 +1,37 @@ +// This file is used to load triggers and notifications when your ZenPack is +// installed. If existing triggers and notifications are found with the same +// name, they will be updated with the new properties specified im this file. +// +// The file should be renamed to actions.json to be picked up during the +// ZenPack's installation. +{ + "triggers": [ + { + "name": "CriticalProductionEvents", + "uuid": "A556B89C-F991-4A29-B7ED-F95643ADFD89", + "enabled": true, + "rule": { + "api_version": 1, + "source": "(prodState >= 1000) and (eventState >= 0) and (severity >= 5)", + "type": 1 + } + } + ], + "notifications": [ + { + "id": "ExampleCommand", + "description": "Example command notification.", + "guid": "B4F091A8-F4C0-4C9D-A7A9-AF3AED2BD6C9", + "action": "command", + "enabled": false, + "action_timeout": 60, + "delay_seconds": 330, + "repeat_seconds": 0, + "send_initial_occurrence": true, + "send_clear": false, + "body_format": "echo ${evt/evid} > $$ZENHOME/var/critical_evid.log", + "clear_body_format": "echo ${evt/evid} > $$ZENHOME/var/cleared_evid.log", + "subscriptions": ["A556B89C-F991-4A29-B7ED-F95643ADFD89"] + } + ] +} diff --git a/CONTENT/zep/zep.json.example b/CONTENT/zep/zep.json.example new file mode 100644 index 0000000..db5c270 --- /dev/null +++ b/CONTENT/zep/zep.json.example @@ -0,0 +1,14 @@ +// This file is used to add custom event fields. It will be loaded when the +// ZenPack is installed. +// +// The file should be renamed to zep.json to be picked up during the ZenPack's +// installation. +{ + "EventDetailItem" : [ + { + "name" : "Example", + "key" : "example_field", + "type" : 1 + } + ] +} diff --git a/GNUmakefile.example b/GNUmakefile.example new file mode 100644 index 0000000..01454e1 --- /dev/null +++ b/GNUmakefile.example @@ -0,0 +1,52 @@ +# GNUmakefile Example + +# A GNUmakefile is only required for ZenPacks that need to bundle external +# dependencies that need to be built when the ZenPack is built instead of when +# the ZenPack is installed. This normally means binary packages that must be +# built, but could also be used to save time at install time. + +# The following template can be used to get started. Note that we want files +# resulting from the build activity to land under the ZENPACK_DIR so that they +# will be included in the resulting ZenPack. You should copy or rename this +# file to "GNUmakefile" if you want it to be automatically executed by the +# ZenPack build and --link installation process. + +PYTHON=python +SRC_DIR=$(PWD)/src +YOURPACKAGE_DIR=$(SRC_DIR)/yourpackage-1.2.3 +ZENPACK_DIR=$(PWD)/ZenPacks/NAMESPACE/PACKNAME +BIN_DIR=$(ZENPACK_DIR)/bin +LIB_DIR=$(ZENPACK_DIR)/lib + +# Default target. This won't be used by any automated process, but would be +# used if you simply ran "make" in this directory. +default: build + +# The build target it specifically executed each time setup.py executes. +# Typically this is when the ZenPack is being built into an egg, or when it is +# installed using the zenpack --link option to install in development mode. +build: + # Example for building a configure+make style dependency. + cd $(YOURPACKAGE_DIR) ; \ + ./configure --prefix=$(ZENPACK_DIR) ; \ + make ; \ + make install + + # Example for building a Python package depedency. + cd $(YOURPACKAGE_DIR) ; \ + PYTHONPATH="$(PYTHONPATH):$(LIB_DIR)" \ + $(PYTHON) setup.py install \ + --install-lib="$(LIB_DIR)" \ + --install-scripts="$(BIN_DIR)" + +# The clean target won't be used by any automated process. +clean: + rm -rf build dist *.egg-info + find . -name '*.pyc' | xargs rm + + # Example for cleaning a configure+make style depedency. + cd $(YOURPACKAGE_DIR) ; make clean + + # Example for cleaning a Python package depdency. + cd $(YOURPACKAGE_DIR) ; rm -rf build dist *.egg-info + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6332391 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +# This graft causes all files located under the ZenPacks/ subdirectory to be +# included in the built ZenPack .egg. Files located in the top-level directory +# of the ZenPack will not be explicitly included. +# +# You can read more about the format and available options available in this +# MANIFEST.in file at the following URL. +# http://docs.python.org/distutils/sourcedist.html +graft ZenPacks diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..6352e25 --- /dev/null +++ b/README.markdown @@ -0,0 +1,164 @@ +# ZenPack Template +This README describes the structure of the ZenPack template that gets +automatically created by Zenoss when you add a ZenPack through the web +interface. + +## Files +At the top-level a ZenPack must have a setup.py. Almost always a MANIFEST.in +file should exist, and in cases where external dependencies must be built for +inclusion in the ZenPack, a GNUmakefile. Examples of these files with inline +comments are included in this template. + +Also included in the ZenPackTemplate is a configure.zcml. As more of Zenoss' +extensibility moves to using ZCA (Zope Component Architecture) this file +becomes crucial to hooking into various aspects of Zenoss. + +## Files and Subdirectories +The following sections describe the purpose and use for each of the default +subdirectories. Note that if the described functionality is not of use in your +ZenPack it is safe to remove any of the default directories. + +### src/ +The src/ top-level directory in ZenPacks is the conventional place to add +third-party dependencies to your ZenPack. It should only be used as a staging +area to do any build work necessary for the dependency. + +See GNUmakefile (or GNUmakefile.example) for examples of how to have +your third-party dependencies automatically compiled and installed at the right +time and into the right location. + +### ZenPacks/NAMESPACE/PACKNAME/ +The following sections describe the directories contained within the +namespaced ZenPacks/NAMESPACE/PACKNAME/ subdirectories. + +#### bin/ +Any general tools delivered by your ZenPack that would be used by the Zenoss +administrator at the command line should go into this directory by convention. +When the ZenPack is installed all files in this directory will be made +executable. + +#### browser/ +The browser subdirectory should contain all code and configuration that's +specific to the Zenoss web interface. The provided configure.zcml will +automatically load the example browser/configure.zcml and register the +browser/resources/ subdirectory to serve static web content. + +#### daemons/ +All files in the daemons/ subdirectory get special handling. Upon installing +the ZenPack, the following actions will occur. + + 1. The file will be made executable (chmod 0755) + 2. A symlink to the file will be created in $ZENHOME/bin/ + 3. An configuration file will be generated at $ZENHOME/etc/DAEMON_NAME.conf + +Assuming that you don't have a $ZENHOME/etc/DAEMONS_TXT_ONLY file this daemon +will also become part of the normal zenoss start and stop processes. + +You can find an example daemon control script in daemons/zenexample. For most +purposes this file can be renamed to the name of the daemon you want to create +and modified to change the DAEMON_NAME. No other modifications are typically +needed. Note that this example control script does expect to launch the real +daemon code which should be located at ../DAEMON_NAME.py. + +#### datasources/ +Any new datasource types you want to add must be added as classes into the +datasources/ subdirectory. When Zenoss is building the list of available +datasources it will scan the datasources/ subdirectory for all installed +ZenPacks. + +An example datasource at datasources/ExampleDataSource.py.example. + +#### lib/ +The lib/ directory should be the installation target for any third-party +libraries that are built by the GNUmakefile. It can also be used as the +conventional location to drop Python-only libraries that don't require +any compilation or special installation. + +#### libexec/ +Any scripts executed by COMMAND datasources in your ZenPack go in this +directory by convention. When the ZenPack is installed all files in this +directory will be made executable. + +#### migrate/ +ZenPacks can include migrate scripts that allow you to run custom code to +handle any tasks that are needed to upgrade your ZenPack from one version to +another. All .py files in this migrate/ subdirectory will be evaluated when the +ZenPack is installed. + +You can find an example migrate script at migrate/ExampleMigration.py. + +#### modeler/ +Any modeler plugins distributed with your ZenPack must be located under the +plugins/ subdirectory. The directory structure and filenames under plugins/ +map directly to the plugins' name in the user interface. For example, if you +wanted to create a modeler plugin called "community.snmp.ExampleMap" you would +create the following directory structure. + +It is recommended that the first portion of the namespace be a short lowercase +form of your name, or organization's name. Alternatively you can choose to use +"community" if you plan to publish the ZenPack and are open to outside +contributions. Zenoss, Inc. will always use "zenoss." The second portion of the +namespace can be the protocol that is used to collect the data. If you are not +using a common protocol it is acceptable to skip the second portion of the +namespace and have something like "community.MongoDB" instead. + +plugins/ + __init__.py + community/ + __init__.py + snmp/ + __init__.py + ExampleMap.py + +Note that the __init__.py files must exist and should be empty files. Otherwise +your modeler plugins won't be imported and usable within Zenoss. + +#### objects/ +All .xml files in this objects/ directory will be loaded into the object +database when the ZenPack installs. All of the objects defined in the XML files +will be automatically associated with the ZenPack. + +When you export the ZenPack from the user interface all objects associated with +the ZenPack will be exported into a file called "objects.xml" specifically. For +this reason it is recommended to let Zenoss manage the objects.xml file and to +never manually create or modify any .xml files in this directory unless you +know what you're doing. + +When a ZenPack is removed, any objects associated with the ZenPack will be +recursively removed from Zenoss. For example, if you associated the /Server +device class with your ZenPack and removed the ZenPack, the /Server device +class, and all devices within it would also be deleted. + +When a ZenPack is upgraded, or re-installed on top of itself, all objects in +the XML files are overlaid on the existing object database. This results in a +merge of the existing objects and what are defined in the XML files with the +XML file properties and relationships winning any conflicts. + +#### reports/ +Custom reports will be loaded from this directory when the ZenPack is +installed. Subdirectories (with the exception of plugins/) will be mapped +directly to the report folders in the web interface. So if you add a .rpt file +into a subdirectory named "Performance Reports" you will find your report in +the Performance Reports folder in the web interface after installing the +ZenPack. + +The plugins/ subdirectory should include any Python plugins your custom reports +call. So if your .rpt file contains a line such as the following.. + +objects python:here.ReportServer.plugin('myplugin', tableState); + +There should be a corresponding myplugin.py file in the plugins/ subdirectory. + +You can find an example report at Example Reports/Example Report.rpt.example +that uses a plugin which can be found at plugins/example_plugin.py. + +#### services/ +ZenHub services will be loaded from the services/ directory. These services +run inside the zenhub daemon and are responsible from all interaction with +collector daemons. + +You can find an example service at services/ExampleConfigService.py. + +#### tests/ +All unit tests for your ZenPack should live in this directory. You can find an +example test suite at tests/testExample.py. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c00933f --- /dev/null +++ b/setup.py @@ -0,0 +1,79 @@ +################################ +# These variables are overwritten by Zenoss when the ZenPack is exported +# or saved. Do not modify them directly here. +# NB: PACKAGES is deprecated +NAME = '' +VERSION = '1.0.0' +AUTHOR = '' +LICENSE = '' +NAMESPACE_PACKAGES = [] +PACKAGES = [] +INSTALL_REQUIRES = [] +COMPAT_ZENOSS_VERS = '' +PREV_ZENPACK_NAME = '' +# STOP_REPLACEMENTS +################################ +# Zenoss will not overwrite any changes you make below here. + +import os +from subprocess import Popen, PIPE +from setuptools import setup, find_packages + +# Run "make build" if a GNUmakefile is present. +if os.path.isfile('GNUmakefile'): + print 'GNUmakefile found. Running "make build" ..' + p = Popen('make build', stdout=PIPE, stderr=PIPE, shell=True) + print p.communicate()[0] + if p.returncode != 0: + raise Exception('"make build" exited with an error: %s' % p.returncode) + +setup( + # This ZenPack metadata should usually be edited with the Zenoss + # ZenPack edit page. Whenever the edit page is submitted it will + # overwrite the values below (the ones it knows about) with new values. + name=NAME, + version=VERSION, + author=AUTHOR, + license=LICENSE, + + # This is the version spec which indicates what versions of Zenoss + # this ZenPack is compatible with + compatZenossVers=COMPAT_ZENOSS_VERS, + + # previousZenPackName is a facility for telling Zenoss that the name + # of this ZenPack has changed. If no ZenPack with the current name is + # installed then a zenpack of this name if installed will be upgraded. + prevZenPackName=PREV_ZENPACK_NAME, + + # Indicate to setuptools which namespace packages the zenpack + # participates in + namespace_packages=NAMESPACE_PACKAGES, + + # Tell setuptools what packages this zenpack provides. + packages=find_packages(), + + # Tell setuptools to figure out for itself which files to include + # in the binary egg when it is built. + include_package_data=True, + + # The MANIFEST.in file is the recommended way of including additional files + # in your ZenPack. package_data is another. + #package_data = {} + + # Indicate dependencies on other python modules or ZenPacks. This line + # is modified by zenoss when the ZenPack edit page is submitted. Zenoss + # tries to put add/delete the names it manages at the beginning of this + # list, so any manual additions should be added to the end. Things will + # go poorly if this line is broken into multiple lines or modified to + # dramatically. + install_requires=INSTALL_REQUIRES, + + # Every ZenPack egg must define exactly one zenoss.zenpacks entry point + # of this form. + entry_points={ + 'zenoss.zenpacks': '%s = %s' % (NAME, NAME), + }, + + # All ZenPack eggs must be installed in unzipped form. + zip_safe=False, +) diff --git a/src/placeholder.txt b/src/placeholder.txt new file mode 100644 index 0000000..e69de29