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