522 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			522 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| =head1 NAME
 | ||
| 
 | ||
| App::Netdisco::Manual::WritingWebPlugins - Documentation on Web 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 Netdisco
 | ||
| can generate the report for you 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 Template::Toolkit files in the regular template
 | ||
| include paths: either its internal locations, or those configured with the
 | ||
| C<site_local_files> setting or the C<register_template_path> helper (see
 | ||
| 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 Template::Toolkit files in the regular template
 | ||
| include paths: either its internal locations, or those configured with the
 | ||
| C<site_local_files> setting or the C<register_template_path> helper (see
 | ||
| below). The template must be called "C<device_port_column.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 Database Connections
 | ||
| 
 | ||
| The Netdisco database is available via the C<netdisco> schema key, as below.
 | ||
| You can also use the C<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 L<DBIx::Class> Schema and also set C<schema_class> to
 | ||
| its name within the C<external_databases> setting, for the second example
 | ||
| above.
 | ||
| 
 | ||
| =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.
 | ||
| 
 | ||
| If you don't plan on redistributing the plugin via CPAN, then configuring the
 | ||
| "C<site_local_files>" setting to be true will enable "C</nd-site-local/lib>"
 | ||
| for Perl code and "C</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 "C<views>" subdirectory of 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.
 | ||
| 
 | ||
| Netdisco searches all template include paths, both those built into the
 | ||
| application and those configured in your plugin(s) with "C<site_local_files>"
 | ||
| or C<register_template_path>.
 | ||
| 
 | ||
| Within the template location, 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');
 | ||
| 
 | ||
| =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 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 "C<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 "C<views>" subdirectory created within "C<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
 | ||
| 
 | ||
| =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
 | ||
| 
 |