Archived community.zenoss.org | full text search
Skip navigation
Currently Being Moderated

Example: Creating New Menu Items

VERSION 1 
Created on: Sep 14, 2009 11:16 AM by Noel Brockett - Last Modified:  Apr 20, 2010 3:59 PM by Matt Ray

Adding a new menu item to Zenoss

In this example, our server name is zenoss1

Our goal here is to add a "Download ZenPack..."  function, in the
ZenPacks tab under the "Settings" navigation item.

Pre-requisites:
- you need to know python
- you need to read the Zope Book.  Unfortunately, there really is no
way shortcut -- you need to read at least 300 pages of the 400 page manual.

============
Log into the Zenoss
Under the "Management" area on the Navigation pane, select "Settings"
This will show a tabbed panel. From here select the "Menus" tab.
In this tab, we need to specify the "Menu ID" of the menu that we want
to change, in this case the "ZenPack_list" menu.  If we weren't sure
about the menu item, we'd need to work through tiral and error to
discover which menu item is the one we want.

In the table menu, select "Add menu...".  This brings up a dialog box.

In the "ID:" field, add give a name that uniquely identifies our menu
item.  For the sake of sanity when we come back to this six months
from now, we'll make it obnoxiously long.
In this case, "menu_download_install_zenpack".

In the "Description:" field, supply the label that you want to appear
when you click the menu.  We'll use "Download..." as our label.  We
could use a label of "Download and install ZenPack...", but it doesn't
fit very well on the screen.

In the "Action:" field, we need to specify the function that we're
going to use when we select this menu item.  We haven't created one
yet, but we will later.
We'll use the action, "dialog_download_install_ZenPacks" as the name
of our function.

The "Ordering:" field refers to where this item will appear in our
list.  Since we want our item to be first in the list, the ordering
value should be the high number, for example "90".

Click on "OK".

Now let's go see if we guessed right about where the menu item shows
up.  Click on "ZenPacks" section.
Yay!  It's there!  What does it do?

It gives us an error, because there's no ZPT called
"dialog_download_install_ZenPacks".  Let's go fix that.

===========
Now here's where things start to get scary...

We're going to need to do the following:
* Let Zope know that our dialog box is actually a dialog box
* Create a Zope Page Template (ZPT or PT).  This template describes
the layout of the dialog box, and what actions to take when we click on the "Ok" button.
* Create the script actions to determine the available ZenPacks
* Create the script actions to download and install the chosen ZenPacks

Go to the Zope Management Interface (ZMI) (this may require you to log
in first as an admin user):

http://zenoss1:8080/zport/manage

From here, click on the following links: dmd, zenMenus, ZenPack_list,
zenMenuItems

NB: Alternatively, you can type in the URL directly, but the scenic route is better to explore everything.

Click on the menu item that we just created ("menu_download_install_zenpack").  We are now in a spot where there are tabs at the top reading "Contents", "Security", "Ownership", and "Properties".  Click on the "Properties" tab.  In the properties tab we can change the text that gets displayed ("description"), the function that gets called when we select the item ("action"), and down at the bottom, the way that our about-to-be-created new page template will display on the screen ("isdialog").  Click on the "isdialog" box and then click the "Save Changes" button.


Now we'll create the Zope Page Template (ZPT) for our dialog box.  It's  better to copy and paste an existing example and update it, so that's what we'll do.  [ -- fast forward -- Search, find, modify, debug, 'Aha!' ]  Okay, so here's how we're going to add this page template.

Go to the "/zport/portal_skins/custom" directory in the ZMI, and go to the top right hand portion of the screen.  Select "Page Template" and click on the "Add" button.  It will ask us for an id (give it "dialog_download_install_zenpacks") and optionally a file.  Click on the "Add and Edit" button.

Here's where we add our finished page template.  Copy and paste the following into the editing portion of the page:

<h2>Install ZenPack</h2>
<p>Select the ZenPacks to download and install </p>
<br/>

<p style="text-align:left;">
    <select class="tableheader" multiple="1" name="chosen_zenpacks" size='5' tal:define="zenpacks here/downloadableZenPacks" tal:on-error="string:Can't find ZenPacks">
     <option tal:repeat="zenpack_id zenpacks" tal:content="zenpack_id" selected="1" />
    </select>
</p>
<div id="dialog_buttons">
    <input type="submit" name="downloadZenPacks:method"
            value="Install"
        tal:attributes="onclick string:return $$('dialog').submit_form('${here/absolute_url_path}')" />

    <input id="dialog_cancel"
            type="button" value="Cancel"
            onclick="$('dialog').hide()"/>
</div>

   Let's take a quick look at the above.  The first thing we should notice is that it is essentially a snippet of HTML with some extra attributes.  The 'tal' (Template Attribute Language) tags allow this HTML-looking page to add variables ("tal:define=...") and loops ("tal:repeat").  Neat.

  This dialog shows us a selectable list of items that is scrollable.  The list of ZenPacks is dynamically chosen.  We can choose more than one item, and by default, everything is selected.

  The list of ZenPacks is determined by the "downloadableZenPacks" function, which we'll need to define.

  Another thing to notice is that the "submit" button calls an 'onclick' method (through the "tal:attributes='onclick ..." line) as well as the target of the button ("downloadZenPacks").  The 'onclick' method is a nice Javascript method that, uhh, does something (ie "we don't care" .  The "downloadZenPacks" is the thing that we actually want to be invoked when someone pushes the button.  Again, this function doesn't exist yet, so we'll need to do that -- soon.


First things first -- let's get the list of ZenPacks. 

We'll go back to the "/zport/portal_skins/custom" directory and keep everything there.  Make a script object by going to the area on the right hand side of
the right-hand pane, underneath the tabs.  Below this area there is an
"Add" button with a pull-down list.  From this pull-down list, select "Script (Python)".  This will prompt you for an Id, and we'll feed it "downloadableZenPacks".

We will now have a new script object that we can use to fill in the
blanks.  At this point, we haven't got the ability to select just one
thing, so we'll just dump out everything.
The "Title" we can add a nice descriptive comment about the code's
purpose -- "Get the list of ZenPacks from the net" should do nicely.

Copy-n-paste in the following code:

#
# Need to compare this against the list of ZenPacks already installed
#
#  Challenges:
#   - how to deal with upgrades and versions
#   - how to deal with packages with the same name from different sites
#
#
#  Infrastructure challenges:
#   - notifications (eg RSS) vs scheduling of checks
#   - download checksum verifications
#   - hosting the ZenPacks
#   - quality checks for non-Zenoss ZenPacks
#

#
# For the moment, we're not going to do anything complicated.
# Look at the name, and if the name exists, then we'll download it.
# Upgrades will need to be manually determined, which will then require
# a remove/install cycle.
#
loaded_zenpacks= context.packs.objectIds()

#
# Get the list of ZenPack URLs
#
available_zenpacks= context.downloadableZenPacks_external()

#
#  As we don't have information on the organization etc, we're
# going to make the simplifying assumption that if two ZenPacks
# have the same name, then they are the same ZenPack.   I don't
# know a lot about namespace modifiers etc, so I don't know how
# to resolve this really nicely.  Doh!
#
net_zenpacks= []
for url in available_zenpacks:
      zenpack_zip= url.split( '/' )[-1]
      zenpack_id= zenpack_zip.replace( '.zip', '' )
      if zenpack_id not in loaded_zenpacks:
            net_zenpacks.append( url )

#
# If there's nothing for us to obtain, we should exit with a fadeout
# message reporting that there's no ZenPacks to download.
#
return net_zenpacks



Some very important things to note about our script:
* Zope constrains the python that can be used so that some 'normal'
things don't work.  For instance, file operations do not work.
This is a security thing.  If your python scripts don't work, it may
not be your fault.
   We get around this by calling *another* script ("context.downloadableZenPacks_external()"), one that doesn't exist in Zope, but actually on the Zope server.
* Two hash signs '##' indicate the start of a comment that is
interpreted by Zope.

For our example, we'll need to create an External method that just
happens to be a python script.   For the VMWare image, the scripts
should live in the /home/zenoss/Extensions directory. 

The actual script (that we'll call "download_zenpacks.py") contents that we'll create are (NB: Be careful of spacing, as python is space sensitive):

#!/usr/bin/python
"""
  Look for addons at www.zenoss.com.  The output looks like:

http://site/addon1
http://site/addon2
  ...
http://site/addonn

  Comment messages are generated which are prepended with '#', including
error messages.
"""



import re
import os
import sys
import urllib
from urllib2 import urlopen
from urlparse import urljoin, urlsplit

#
# --- Globals  ---------------------------
#
default_sites= [
   "http://www.zenoss.com/download/links?nt",
]

addon_regexes= {
   'zenpack': r'/download/zenpacks/\w+\.zip',
   'plugin': r'http://downloads.sourceforge.net/zenoss/Zenoss-Plugins-[^"]+',
}

download_dir= "/tmp"

#
# --  Functions  -------------------------
#
def discover_addons( site, addon_regex ):
        """Search the given URL for zenpacks or plugins"""

        #print "# Searching %s ..." % site
        addons= []

        try:
                site_fp= urlopen( site )
                web_page= site_fp.read()
                all_addons= re.findall( addon_regex, web_page )
                for addon in all_addons:
                        if addon.find( "http:" ) == -1:
                                addon= urljoin( site,  addon )

                        addons.append( addon )

        except:
                return [ "There was an error obtaining addon information from %s: %s" % ( site, sys.exc_info()[1] ) ]

        return addons



def showZenPacks():
        addons= []
        regex= addon_regexes[ 'zenpack' ]
        for site in default_sites:
                addons= addons + discover_addons( site, regex )

        return sorted( addons )



def download_addon( addon, download_dir ):
        """Download the addon from the given URL"""

        #print "# Downloading %s ..." % addon
        path= urlsplit( addon )[2]
        file= path.split( '/' )[-1]
        ( filename, headers )= urllib.urlretrieve( addon, download_dir + os.sep + file )

        return filename



def install_zenpack( zenpack ):
        """Actually install the ZenPack"""

        command= "zenpack run --install %s" % zenpack
        os.system( command )
        return ""



def download_install_chosen_zenpacks( self, downloads, REQUEST=None ):
        success= 0
        for addon in downloads:
                try:
                        local_addon= download_addon( addon, download_dir )
                        success= success + 1

                except:
                        return success, "Problems downloading the addon from %s: %s" % ( addon, sys.exc_info()[1] )

                msg= install_zenpack( local_addon )
                if msg != "":
                        return success, msg

        self._p_jar.sync()
        return success, ""

#
# --  End of script  ---------------------
#

NB: Make sure that the script has the excutable bit set.  (eg "chmod 755" should do the trick)

Now we need to create a link in Zope to a particular function in our python script.  Go back to "/zport/portal_skins/custom" and add an "External Method" (ie call a function in a script on the Zope server's filesystem).  The id that we'll give it is "downloadableZenpacks_external", which matches what was called from the "downloadableZenPacks" Script (Python).  The module name "download_zenpacks" is the name of the script ("/home/zenoss/Extensions/download_zenpacks.py"). The function that we'll call is "showZenPacks".  Click on the "Save Changes" button.

At this point we have a dialog box that should show us a list of ZenPacks from the Zenoss web site.  If you've already installed all of the ZenPacks, then you'll need to uninstall at least one ZenPack, as the Script (Python) method ("downloadableZenPacks") won't show you anything that has already been installed.  In a browser that has a Zenoss session in it, go to the "Settings" section, click on the "ZenPacks" tab and click on the "Download..." menu item.  If everything has worked, then you will see a list of ZenPacks that you can download.  Click on the "Cancel" button as we haven't go any code to do anything useful yet.

Okay, now for the home stretch!  Let's add the action that gets invoked when you click on the "Ok" button.

Create a "downloadZenPacks" Script (Python) in the "/zport/portal_skins/custom" directory.  In the "Parameter List" area, put in the parameters:

chosen_zenpacks=None, REQUEST=None

  and then add the following code into the editable area:

#
# If the user only selects one item, it gets returned as a string rather than
# as an array with one item.  Sigh.  Convert it to an array.
#
if isinstance( chosen_zenpacks, str ):
   chosen_zenpacks= [ chosen_zenpacks ]

#
# Call the external method which will actually do the work
#
successes, message= context.downloadZenPacks_external( chosen_zenpacks )

installed= "Downloaded and installed %d of %d ZenPacks" % ( successes, len( chosen_zenpacks ) )

if message != "":
   message= installed + "\n" + message
else:
   message= installed

if REQUEST is not None:
   REQUEST.RESPONSE.redirect( '/zport/dmd/viewZenPacks?message=%s' % message )


#
# The following, with the sync call in the external module, seem to
# refresh the page.
#
zpt_zenpacks_tab= context.viewZenPacks
return zpt_zenpacks_tab()

   Okay, now we need to create the external method that actually does the download and runs the install.  In the "/zport/portal_skins/custom" directory in the ZMI, create a new "External Method".  For the id, we'll use "downloadZenPacks_external", for the module name "download_zenpacks" and for the function, we'll use "download_install_chosen_zenpacks".  Click on "Save Changes".

Now test it out.  If everything is okay, your dialog box should contain the list of ZenPacks that you can download, and when you select the ZenPacks and click on "OK", the download and install should happen.  The screen should also automatically show the new ZenPacks.

Congratulations on the new menu item!

Comments (0)