davidz00's quick and dirty guide to creating your own ZenPack by modifying a previous ony.I leave a LOT out here but by laying out your data like mine and doing some basic find/replace on the files you should have something working soon. This may feel "scattered" and frankly I've done my best to be readable yet to the point. There is so much to cover I'm trying to catch the important parts. This is rough so bear with me.
First figure out what type of extra thing we will be adding. For my example I will be modeling SAN fiber interfaces on a Cisco m9500 series SAN Switch.
Important Note
DOWNLOAD THE FINISHED PLUGIN HERE! YOU WILL NEED THIS TO FOLLOW WITH!!!!!
SanMonitor Zenpack discussed below is attached.
APCMonitor Zenpack NOT discussed at all here but great for comparison if you're in a bind, also attached.
Introduction
What I usually do is follow the "parent -> child" method of thinking when designing the plugin. Figure out what is the parent (SAN device) and what are the children (SAN interfaces)
I make a rough list similar to the one below which I use to keep my names straight and avoid wasting time due to checking variable names (my short term memory is horrible)
So I'll need to create an empty zenpack from the zenoss UI. I'm going to name it "SanMonitor"
I'll need two classes to represent the parent and the children. These will be created inside the zenpack root directory inside your Zenoss folder.
- $ZENHOME/Products/SanMonitor/SanDevice.py
- $ZENHOME/Products/SanMonitor/SanInterface.py
I'll need a modeler plugin to "model" the device and create the children and assign them their values.
- $ZENHOME/Products/SanMonitor/modeler/plugins/CiscoSAN.py
I'll need two skin files to show the interfaces from the device, and then another one to show the graphs on a single interface.
- $ZENHOME/Products/SanMonitor/skins/SanMonitor/SanDeviceDetail.pt
- $ZENHOME/Products/SanMonitor/skins/SanMonitor/viewSanInterface.pt
Now here is my quick scratch list which helps me keep things organized shown below. Its much easier since you will be juggling different files all with different syntax and be required to use the exact case sensitive variable names.
I'll try to describe everything in a bit more detail later.
Products.SanMonitor
Package name: Products.SanMonitor
Device Class: SanDevice.py
View: SanDeviceDetail.pt
Relationship name: SanDev
RelationshipChildren: SanInt -> Products.SanMonitor.SanInterface
# PARENT-OF RELATIONSHIP (actual "relationship code")
_relations = Device._relations + (
('SanInt', ToManyCont(ToOne,
'Products.SanMonitor.SanInterface', 'SanDev')),
)
Component Class: SanInterface.py ::
View: viewSanInterface.pt
Relname: SanInt
RelParent: SanDev -> Products.SanMonitor.SanDevice
# CHILD-OF RELATIONSHIP (actual "relationship code")
_relations = (
("SanDev", ToOne(ToManyCont,
"Products.SanMonitor.SanDevice", "SanInt")),
)
Device View skin: SanDeviceDetail.pt
Component View skin: viewSanInterface.pt
Relationship model simplified
Find/Replace these uppercase names below to simplify things
PARENT_CLASS = SanDevice
PARENT_RELATION = SanDev
CHILD_CLASS = SanInterface
CHILD_RELATION = SanInt
ZENPACK_NAME = SanMonitor
# PARENT-OF RELATIONSHIP (actual "relationship code")
_relations = Device._relations + (
('CHILD_RELATION', ToManyCont(ToOne,
'Products.ZENPACK_NAME.CHILD_CLASS', 'PARENT_RELATION')),
)
# CHILD-OF RELATIONSHIP (actual "relationship code")
_relations = (
("PARENT_CLASS", ToOne(ToManyCont,
"Products.ZENPACK_NAME.PARENT_CLASS", "CHILD_RELATION")),
)
Now by doing some basic find/replace on the plugin I post, and changing the filenames and relationship names you should be able to get something together. Just go through the files one at a time and change all occurrences to your own.
Once you've edited your files and done the swapping your next task is to edit the modeler plugin. Find the TableMap? code and replace the oids with your table and variables. Also update the relationship to point at the child.
You only need to define the data you want to save. So if you have 6 datapoints but you only want data from the first three your table map would look like this.
I'll describe everything and what it is below.
# fcFxPortPhysEntry is the name of the table, anything you want
# to name it is fine, just remember to call it
# from process() below with the right name.
#
# .1.3.6.1.2.1.75.1.2.2.1 is the oid of the table. Everything AFTER
# this returned is saved as the oid variable
#
# {'.1': 'AdminStatus', where '.1' is the oid to match
#
# and 'AdminStatus' is the attribute name
# used later. CHILD_CLASS/AdminStatus would
# give us the data from the skin .pt file
#
# Physical Port Table
GetTableMap('fcFxPortPhysEntry', '.1.3.6.1.2.1.75.1.2.2.1',
{'.1': 'AdminStatus',
'.2': 'OperStatus',
'.3': 'LastChange'}
),
I know you're wondering where the index is, its actually saved in the Process() function. We'll get to that, for now forget about it and focus on the mib oids as defined.
Remember: Oids returned that are not defined will be discarded. If you need it, define it here
You can also walk a larger table and be more specific like so. Notice the removal of the ending .1 from the above example and the addition below. Everything still matches but this could be slower on huge tables.
# Physical Port Table
GetTableMap('fcFxPortPhysEntry', '.1.3.6.1.2.1.75.1.2.2',
{'.1.1': 'AdminStatus',
'.1.2': 'OperStatus',
'.1.3': 'LastChange'}
# also possible '.2.1': 'attributename'
# '.4.5': 'othervariable'
),
Thats important because your life will be easier if you can walk one big table instead of walking many smaller tables.
The names you assigned to an oid will directly be attributes on the class now. So you could call SanInterface.AdminStatus and it would work. Now we wont call them that way but we will call them from the skin files. That data is the same data snmp gave you, but if you want to format it then you need to create methods inside SanInterface.py which can be called as well. Useful for converting timeticks to HH:MM:SS etc. (check out getLastChange() inside SanInterface.py)
Now we need to start working on the skin files. These suck and are rather difficult to work with . I'd suggest being very careful and changing very little here. Change only things that look like the names I've gave above including the name "Interface" and some other areas.
(SanInt being the new child relationship)
objects here/SanInt/objectValuesAll;
(Further down, change "Interface" to something you come up with) etc.
<tal:block tal:repeat="Interface batch">
<tr tal:define="odd repeat/Interface/odd"
Some special features you might use would be the green/red/grey dots for status. This block of code handles that. If it == 1 then green else red. It still defines data and must be in order with the table.
<td class="tablevalues" align="center">
<img border="0"
tal:attributes="src python:test(Interface.OperStatus==1,
here.getStatusImgSrc(0),
here.getStatusImgSrc(3))" />
</td>
If you dont want to use those then replace them with code like this
<td class="tablevalues" tal:content="Interface/getLastChange">12:34:56</td>
Basically just Interface/Datapoint or Interface/Function
If you're still wondering where just know that everything must flow in order and it lines up. For example.
<td class="tableheader" width="10" align=center>Interface</td>
<td class="tableheader" width="20" align=center>Last Change</td>
<td class="tableheader" width="5" align=center>OperStatus</td>
<td class="tableheader" width="5" align=center>AdminStatus</td>
would require the same order for the values defined below here.
<!-- LINK TO CHILD_CLASS SKIN for Interface -->
<td class="tablevalues">
<a class=tablevalues tal:content="Interface/name"
tal:attributes="href Interface/getPrimaryUrlPath">BlaBla</a>
</td>
<!-- Calls the pretty formatting function to show LastChange -->
<td class="tablevalues" tal:content="Interface/getLastChange">12:34:56</td>
<!-- Shows fancy status indicators for OperStatus (0=green,3=red)-->
<td class="tablevalues" align="center">
<img border="0"
tal:attributes="src python:test(Interface.OperStatus==1,
here.getStatusImgSrc(0),
here.getStatusImgSrc(3))" />
</td>
<!-- Shows fancy status indicators for AdminStatus (0=green,3=red) -->
<td class="tablevalues" align="center">
<img border="0"
tal:attributes="src python:test(Interface.AdminStatus==1,
here.getStatusImgSrc(0),
here.getStatusImgSrc(3))" />
</td>
After you've finished with your skins you need to figure out where in the zenoss path they will live. Like /Devices/Firewall/ for example.
Set the zPythonClass property to Products.ZenPackName?.DeviceClassFile? You will need to add devices DIRECTLY to this path from the add device area. Moving them after adding elsewhere will not work right. Signs of this include the "no relationship on device" warnings from the modeler.
If the devices already exist and give that warning you need to remove them and re add OR using zendmd do the following. (Change the import)
from Products.SanMonitor.SanDevice import SanDevice
find("DEVICE NAME HERE").__class__ = SanDevice
find("DEVICE NAME HERE").buildRelations()
commit()
Make sure you set that Device Path to use the new modeler plugin as well. (/Devices/SAN/Cisco -> Collector Plugins)
Once everything is modeled you can now create templates for the components. Just make sure they have the same name as the component class file. These dont get bound like other templates, the name will bind it to the class automagically. For every oid in the template the value of snmpindex is appended to the end. Remember the value we saved in the modeler?
A few gotchas to remember.
Editing any .pt skin file requires a zope restart to take effect. zopectl restart as the zenoss user.
Modeler plugin refusing to run even when in the list of modelers usually means your plugin has errors. double check your code.
Modeler warnings about missing relations means you added the device wrong. re add it but use the dropdown box to manually place it in the folder where zPythonClass was set.
in some rare cases with templates binding to components I've had to do a restart of zenoss entirely to get results. zenoss restart
If it still doesnt work and you're clueless, start checking all the logs. Place log.info("made it to this function") lines in your modeler to help show you where its at before things break.
Anyway thanks for reading this scattered crazy document. Something better will come eventually. This is geared towards those who know 'almost' enough to get their first plugin out.