476 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			476 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| =head1 NAME
 | |
| 
 | |
| App::Netdisco::Manual::WritingPlugins - Documentation on Plugins for Developers
 | |
| 
 | |
| =head1 Introduction
 | |
| 
 | |
| L<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 L<App::Netdisco::Web::Plugin> for more general information about plugins.
 | |
| 
 | |
| =head1 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 L<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 L<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;
 | |
| 
 | |
| =head1 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 C</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.
 | |
| 
 | |
| =head1 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 C<App::Netdisco::Web::Plugin::Device> or
 | |
| C<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
 | |
| L<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
 | |
| L<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'});
 | |
| 
 | |
| =head1 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 you can
 | |
| generate the report without any Perl or HTML: see the L<Reports
 | |
| Configuration|App::Netdisco::Manual::Configuration/reports> 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 C<App::Netdisco::Web::Plugin::Report>
 | |
| namespace. To register a handler for display as a Report, you need to pick the
 | |
| I<category> of the report. Here are the pre-defined categories:
 | |
| 
 | |
| =over 4
 | |
| 
 | |
| =item *
 | |
| 
 | |
| Device
 | |
| 
 | |
| =item *
 | |
| 
 | |
| Port
 | |
| 
 | |
| =item *
 | |
| 
 | |
| IP
 | |
| 
 | |
| =item *
 | |
| 
 | |
| Node
 | |
| 
 | |
| =item *
 | |
| 
 | |
| VLAN
 | |
| 
 | |
| =item *
 | |
| 
 | |
| Network
 | |
| 
 | |
| =item *
 | |
| 
 | |
| Wireless
 | |
| 
 | |
| =back
 | |
| 
 | |
| 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 L<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 C<hidden> key:
 | |
| 
 | |
|  register_report({
 | |
|    tag   => 'newfeature',
 | |
|    label => 'My New Feature',
 | |
|    hidden => true,
 | |
|  });
 | |
| 
 | |
| =head1 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 C<register_search_tab>,
 | |
| C<register_device_tab>, or C<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 C<is_ajax> call is part of the standard Dancer featureset.
 | |
| 
 | |
| =head1 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 C</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 C<hidden> key:
 | |
| 
 | |
|  register_admin_task({
 | |
|    tag   => 'newfeature',
 | |
|    label => 'My New Feature',
 | |
|    hidden => true,
 | |
|  });
 | |
| 
 | |
| =head1 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 one Template::Toolkit file in the regular template
 | |
| include paths (see also C<register_template_path>, below). The template must
 | |
| be called "C<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 L<App::NetdiscoX::Web::Plugin::Observium>
 | |
| distribution.
 | |
| 
 | |
| =head1 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 one Template::Toolkit file in the regular template
 | |
| include paths (see also C<register_template_path>, below). The template must
 | |
| be called "C<device_details.tt>" on disk and live in the directory:
 | |
| 
 | |
|  plugin/mydevicedetailsplugin/device_details.tt
 | |
| 
 | |
| For a good example of this, see the L<App::NetdiscoX::Web::Plugin::RANCID>
 | |
| distribution.
 | |
| 
 | |
| =head1 User Authorization
 | |
| 
 | |
| All Dancer route handlers must have proper authorization configured. This is
 | |
| not difficult. Make sure that your module loads the
 | |
| L<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 C<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
 | |
| C<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 ("C<< => >>").
 | |
| 
 | |
| =head1 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:
 | |
| 
 | |
|  package App::Netdisco::Web::Plugin::Search::MyNewFeature
 | |
|   
 | |
|  use File::ShareDir 'dist_dir';
 | |
|  register_template_path(
 | |
|    dist_dir( 'App-Netdisco-Web-Plugin-Search-MyNewFeature' ));
 | |
| 
 | |
| The registered path will be searched before the built-in C<App::Netdisco>
 | |
| path. We recommend use of the L<File::ShareDir> module to package and ship
 | |
| templates along with your plugin, as shown.
 | |
| 
 | |
| Each path added using C<register_template_path> is searched I<before> any
 | |
| existing paths in the template config. See the
 | |
| L<App::NetdiscoX::Web::Plugin::Observium> distribution for a working example.
 | |
| 
 | |
| =head2 Template Variables
 | |
| 
 | |
| Some useful variables are made available in your templates automatically by
 | |
| App::Netdisco:
 | |
| 
 | |
| =over 4
 | |
| 
 | |
| =item  C<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.
 | |
| 
 | |
| =item  C<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.
 | |
| 
 | |
| =item  C<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.
 | |
| 
 | |
| =item  C<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"/>
 | |
| 
 | |
| =item  C<uri_for>
 | |
| 
 | |
| Simply the Dancer C<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') %]" ...>
 | |
| 
 | |
| =item  C<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" ...>
 | |
| 
 | |
| =back
 | |
| 
 | |
| =head1 Javascript and Stylesheets
 | |
| 
 | |
| A simple mechanism exists for loading additional Javascript and CSS documents.
 | |
| This is done in the C<< <head> >> section of the web page.
 | |
| 
 | |
| Within a Template include path (see C<register_template_path>, above) create a
 | |
| directory called "C<plugin>" and within that another directory named after
 | |
| your plugin (e.g. "C<mynewfeature>"). The Javascript and/or CSS files must
 | |
| then be named "C<mynewfeature.js>" and "C<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');
 | |
| 
 | |
| Note that this searches all template include paths, both those built into the
 | |
| application and those configured in your plugin(s) with
 | |
| C<register_template_path>.
 | |
| 
 | |
| =head1 Naming and File Location
 | |
| 
 | |
| There are several options for how you name, distribute and install your
 | |
| App::Netdisco plugin.
 | |
| 
 | |
| =head2 Namespaces
 | |
| 
 | |
| As mentioned in L<App::Netdisco::Web::Plugin>, official Netdisco plugins live
 | |
| in the C<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 C<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.
 | |
| 
 | |
| =head2 File Location
 | |
| 
 | |
| If writing your own plugins, Netdisco supports a local include path which is
 | |
| usually C<~/site_plugins> (or C<${NETDISCO_HOME}/site_plugins>).
 | |
| 
 | |
| This means if your plugin is called
 | |
| "App::NetdiscoX::Web::Plugin::MyPluginName" then it could live at:
 | |
| 
 | |
|  ~/site_plugins/App/NetdiscoX/Web/Plugin/MyPluginName.pm
 | |
| 
 | |
| This feature should make development of new plugins or maintenance of local
 | |
| plugins much more straighforward.
 | |
| 
 | |
| =head1 Plugin Configuration
 | |
| 
 | |
| You can support new configuration items which the user should add to their
 | |
| C<~/environments/deployment.yml> file. Please use a single Hash-Ref option
 | |
| named "C<plugin_mypluginname>" (if your plugin's called C<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 C<settings> key:
 | |
| 
 | |
|  <a href="http://[% settings.plugin_observium.webhost | uri %]/>Observium</a>
 | |
| 
 | |
| =cut
 | |
| 
 |