NAME
App::Netdisco::Manual::WritingPlugins - Documentation on Plugins for Developers
Introduction
App::Netdisco's plugin subsystem allows developers to write and test web user interface (UI) components without needing to patch the main Netdisco application. It also allows the end-user more control over the UI components displayed in their browser.
See App::Netdisco::Web::Plugin for more general information about plugins.
Developing Plugins
A plugin is simply a Perl module which is loaded. Therefore it can do anything you like, but most usefully for the App::Netdisco web application the module will install a Dancer route handler subroutine, and link this to a web user interface (UI) component.
Explaining how to write Dancer route handlers is beyond the scope of this document, but by examining the source to the plugins in App::Netdisco you'll probably get enough of an idea to begin on your own.
App::Netdisco plugins should load the App::Netdisco::Web::Plugin module. This exports a set of helper subroutines to register the new UI components. Here's the boilerplate code for our example plugin module:
package App::Netdisco::Web::Plugin::MyNewFeature
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
# plugin registration code goes here, ** see below **
# your Dancer route handler
get '/mynewfeature' => require_login sub {
# ...lorem ipsum...
};
true;
Navigation Bar items
These components appear in the black navigation bar at the top of each page, as individual items (i.e. not in a menu). The canonical example of this is the Inventory link.
To register an item for display in the navigation bar, use the following code:
register_navbar_item({
tag => 'newfeature',
path => '/mynewfeature',
label => 'My New Feature',
});
This causes an item to appear in the Navigation Bar with a visible text of "My New Feature" which when clicked sends the user to the /mynewfeature
page. Note that this won't work for any target link - the path must be an App::Netdisco Dancer route handler. Please bug the App::Netdisco devs if you want arbitrary links supported.
Search and Device page Tabs
These components appear as tabs in the interface when the user reaches the Search page or Device details page. Note that Tab plugins usually live in the App::Netdisco::Web::Plugin::Device
or App::Netdisco::Web::Plugin::Search
namespace.
To register a handler for display as a Search page Tab, use the following code:
register_search_tab({tag => 'newfeature', label => 'My New Feature'});
This causes a tab to appear with the label "My New Feature". So how does App::Netdisco know what the link should be? Well, as the App::Netdisco::Developing documentation says, tab content is retrieved by an AJAX call back to the web server. This uses a predictable URL path format:
/ajax/content/<search or device>/<feature tag>
For example:
/ajax/content/search/newfeature
Therefore your plugin module should look like the following:
package App::Netdisco::Web::Plugin::Search::MyNewFeature
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_search_tab({tag => 'newfeature', label => 'My New Feature'});
get '/ajax/content/search/newfeature' => require_login sub {
# ...lorem ipsum...
# return some HTML content here, probably using a template
};
true;
If this all sounds a bit daunting, take a look at the App::Netdisco::Web::Plugin::Search::Port module which is fairly straightforward.
To register a handler for display as a Device page Tab, the only difference is the name of the registration helper sub:
register_device_tab({tag => 'newfeature', label => 'My New Feature'});
Reports
Report components contain pre-canned searches which the user community have found to be useful. Before you go further, it might be the case that Netdisco can generate the report for you without any Perl or HTML: see the Reports Configuration for details.
Otherwise, the typical implementation is very similar to one of the Search and Device page Tabs, so please read that documentation above, first.
Report plugins usually live in the App::Netdisco::Web::Plugin::Report
namespace. To register a handler for display as a Report, you need to pick the category of the report. Here are the pre-defined categories:
Device
Port
IP
Node
VLAN
Network
Wireless
Once your category is selected, use the following registration code:
register_report({
category => 'Port', # pick one from the list
tag => 'newreport',
label => 'My New Report',
});
You will note that like Device and Search page Tabs, there's no path specified in the registration. The reports engine will make an AJAX request to the following URL:
/ajax/content/report/<report tag>
Therefore you should implement in your plugin a route handler for this path. The handler must return the HTML content for the report. It can also process any query parameters which might customize the report search.
See the App::Netdisco::Web::Plugin::Report::DuplexMismatch module for a simple example of how to implement the handler.
An additional feature allows you to create Reports which do not appear in the Navbar menu. This is useful if the page is only linked directly from another (for example Port Log). To enable this feature add the hidden
key:
register_report({
tag => 'newfeature',
label => 'My New Feature',
hidden => true,
});
CSV Response
Most pages in Netdisco are a table with data. It's possible to have the application add a link to download a CSV version of the same data. To do this, include the following option in your call to register_search_tab
, register_device_tab
, or register_report
:
provides_csv => 1
The other thing you need to do is adjust your Dancer route handler to return either HTML or CSV data. Here's the typical way to do it:
get '/ajax/content/search/newfeature' => require_login sub {
# build some kind of dataset here (e.g. a DBIx::Class query)
if (request->is_ajax) {
template 'mytemplate', { data => $mydataset }, { layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'mytemplate_csv', { data => $mydataset }, { layout => undef };
}
};
Note that the is_ajax
call is part of the standard Dancer featureset.
Admin Tasks
These components appear in the black navigation bar under an Admin menu, but only if the logged in user has Administrator rights in Netdisco.
To register an item for display in the Admin menu, use the following code:
register_admin_task({
tag => 'newfeature',
label => 'My New Feature',
});
This causes an item to appear in the Admin menu with a visible text of "My New Feature" which when clicked sends the user to the /admin/mynewfeature
page. Note that this won't work for any target link - the path must be an App::Netdisco Dancer route handler. Please bug the App::Netdisco devs if you want arbitrary links supported.
An additional feature allows you to create Admin Tasks which do not appear in the Navbar menu. This is useful if the page is only linked directly from another. To enable this feature add the hidden
key:
register_admin_task({
tag => 'newfeature',
label => 'My New Feature',
hidden => true,
});
Device Port Columns
You can also add columns to the Device Ports page. The canonical example of this is to add hyperlinks (or embedded images) of traffic graphs for each port, however the plugin is a regular Template::Toolkit template so can be any HTML output.
The column plugin has a name (used internally to locate files on disk), label (the heading for the column in the table or CSV output), position in the table (three options: left, mid, right), and finally a flag for whether the column is displayed by default.
To register the column call the following helper routine:
register_device_port_column({
name => 'myportcolumnplugin',
label => 'My Port Column Heading',
position => 'left', # or "mid" or "right"
default => 'on', # or undef
});
App::Netdisco searches for Template::Toolkit files in the regular template include paths: either its internal locations, or those configured with the site_local_files
setting or the register_template_path
helper (see below). The template must be called "device_port_column.tt
" on disk and live in the directory:
plugin/myportcolumnplugin/device_port_column.tt
For a good example of this, see the App::NetdiscoX::Web::Plugin::Observium distribution.
Device Details
You can add items to the Device Details tab as well. A good example of this is to add a link to the RANCID backup of the device in a WebSVN app somewhere. Like Device Port Columns plugins, the plugin is a regular Template::Toolkit snippet so can be any HTML output.
The details plugin has a name (used internally to locate files on disk) and label (the heading for the row in the table).
To register the column call the following helper routine:
register_device_details({
name => 'mydevicedetailsplugin',
label => 'My Device Details Heading',
});
App::Netdisco searches for Template::Toolkit files in the regular template include paths: either its internal locations, or those configured with the site_local_files
setting or the register_template_path
helper (see below). The template must be called "device_port_column.tt
" on disk and live in the directory:
plugin/mydevicedetailsplugin/device_details.tt
For a good example of this, see the App::NetdiscoX::Web::Plugin::RANCID distribution.
User Authorization
All Dancer route handlers must have proper authorization configured. This is not difficult. Make sure that your module loads the Dancer::Plugin::Auth::Extensible module (as shown above).
For each route handler you either simply require that a user be logged in, or that the user is an administrator.
To require a logged in user, include the require_login
wrapper:
get '/ajax/content/search/newfeature' => require_login sub {
# etc .....
To require an administrator, specify their role:
get '/ajax/control/admin/newfeature' => require_role admin => sub {
# etc .....
Finally in case you need it, the other role a user can have is port_control
:
ajax '/ajax/portcontrol' => require_role port_control => sub {
# etc .....
Take care over the subtle differences in syntax, especially the placement of the fat comma ("=>
").
Database Connections
The Netdisco database is available via the netdisco
schema key, as below. You can also use the external_databases
configuration item to set up connections to other databases.
# possibly install another database driver
~netdisco/bin/localenv cpanm --notest DBD::mysql
# deployment.yml
external_databases:
- tag: externaldb
dsn: 'dbi:mysql:dbname=myexternaldb;host=192.0.2.1'
user: oliver
password: letmein
# plugin code
use Dancer::Plugin::DBIC;
schema('netdisco')->resultset('Devices')->search({vendor => 'cisco'});
schema('externaldb')->resultset('MyTable')->search({field => 'foobar'});
You'll need to install a DBIx::Class Schema and also set schema_class
to its name within the external_databases
setting, for the second example above.
Templates
All of Netdisco's web page templates are stashed away in its distribution, probably installed in your system's or user's Perl directory. It's not recommended that you mess about with those files.
So in order to replace a template with your own version, or to reference a template file of your own in your plugin, you need a new path.
If you don't plan on redistributing the plugin via CPAN, then configuring the "site_local_files
" setting to be true will enable "/nd-site-local/lib
" for Perl code and "/nd-site-local/share
" for tmplates in your Netdisco home location. You will need to create these directories.
Alternatively, shipping templates within a CPAN distribution, the following code would be appropriate:
package App::Netdisco::Web::Plugin::Search::MyNewFeature
use File::ShareDir 'dist_dir';
register_template_path(
dist_dir( 'App-Netdisco-Web-Plugin-Search-MyNewFeature' ));
The "views
" subdirectory of the registered path will be searched before the built-in App::Netdisco
path. We recommend use of the File::ShareDir module to package and ship templates along with your plugin, as shown.
Each path added using register_template_path
is searched before any existing paths in the template config. See the App::NetdiscoX::Web::Plugin::Observium distribution for a working example.
Template Variables
Some useful variables are made available in your templates automatically by App::Netdisco:
search_node
-
A path and query string which links to the Node tab of the Search page, together with the correct default search options set.
search_device
-
A path and query string which links to the Device tab of the Search page, together with the correct default search options set.
device_ports
-
A path and query sting which links to the Ports tab of the Device page, together with the correct default column view options set.
uri_base
-
Used for linking to static content within App::Netdisco safely if the base of the app is relocated, for example:
<link rel="stylesheet" href="[% uri_base %]/css/toastr.css"/>
uri_for
-
Simply the Dancer
uri_for
method. Allows you to do things like this in the template safely if the base of the app is relocated:<a href="[% uri_for('/search') %]" ...>
self_options
-
Available in the Device tabs, use this if you need to refer back to the current page with some additional parameters, for example:
<a href="[% uri_for('/device', self_options) %]&foo=bar" ...>
Javascript and Stylesheets
A simple mechanism exists for loading additional Javascript and CSS documents. This is done in the <head>
section of the web page.
Netdisco searches all template include paths, both those built into the application and those configured in your plugin(s) with "site_local_files
" or register_template_path
.
Within the template location, create a directory called "plugin
" and within that another directory named after your plugin (e.g. "mynewfeature
"). The Javascript and/or CSS files must then be named "mynewfeature.js
" and "mynewfeature.css
" respectively. For example:
plugin/mynewfeature/mynewfeature.js
plugin/mynewfeature/mynewfeature.css
Tell App::Netdisco that you wish to load one or the other using the following helper routines:
register_javascript('mynewfeature');
register_css('mynewfeature');
Naming and File Location
There are several options for how you name, distribute and install your App::Netdisco plugin.
Namespaces
As mentioned in App::Netdisco::Web::Plugin, official Netdisco plugins live in the App::Netdisco::Web::Plugin::
namespace. You can use this namespace and submit the product to the Netdisco developer team for consideration for inclusion in the official distribution.
Alternatively you can release the plugin to CPAN under your own account. In that case we request that you instead use the App::NetdiscoX::Web::Plugin::
namespace (note the "X"). Users can load such modules by using the abbreviated form "X::MyPluginName" which is then expanded to the full package.
File Location
If writing your own plugins that are not for redistribution or packaging on CPAN, Netdisco can enable local include paths for Perl, templates, and static content such as javascript and images.
Configuring the "site_local_files
" to be "true" enables:
# perl code
$ENV{NETDISCO_HOME}/nd-site-local/lib
# templates and static content
$ENV{NETDISCO_HOME}/nd-site-local/share
Note that you still need to create the directories yourself, and templates may need to have a further "views
" subdirectory created within "share
".
As an example, if your plugin is called "App::NetdiscoX::Web::Plugin::MyPluginName" then it could live at:
~netdisco/nd-site-local/lib/App/NetdiscoX/Web/Plugin/MyPluginName.pm
Plugin Configuration
You can support new configuration items which the user should add to their ~/environments/deployment.yml
file. Please use a single Hash-Ref option named "plugin_mypluginname
" (if your plugin's called mypluginname
). For example:
plugin_observium:
webhost: "web-server.example.com"
open_in_new_window: true
You can then refer to this configuration in your plugin module:
my $webhost = setting('plugin_observium')->{'webhost'};
Or in templates via Dancer's settings
key:
<a href="http://[% settings.plugin_observium.webhost | uri %]/>Observium</a>
1 POD Error
The following errors were encountered while parsing the POD:
- Around line 487:
Non-ASCII character seen before =encoding in '# perl'. Assuming UTF-8