The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

OpenInteract2::Manual::Management - Creating tasks to manage OpenInteract2

SYNOPSIS

This part of the manual describes how create your own tasks using the OpenInteract2::Manage framework.

PREREQUISITES

Before you read this you should have at least a passing familiarity with how OpenInteract2::Manage works. You can see its use in oi2_manage, but since that script uses tasks in a generic fashion it may be a little tougher to follow.

MOTIVATION

Previous environment

In OI 1.x most of the logic to run the management tasks was in the oi_manage script which had multiple problems:

  • You could not call it from anywhere but the command line. For most tasks this was fine, but it prevented any sort of web-based management without simply cobbling together strings to run using system. Yuck.

  • Its size (4100+ lines) also meant that it was difficult to refactor. And it was fairly difficult to add new tasks without doing lots of copy-and-paste and knowing where certain tasks (parameter parsing, etc.) were performed.

  • Documentation always got out of sync. Sometimes the docs didn't list the task at all, sometimes it had the wrong or too few parameters for the task or it, and at other times the description didn't really say what the task was doing.

Turn weaknesses into strengths

The architecture of the management framework in OI2 is exactly the opposite of this. The command-line tool (oi2_manage) is just a shell taking advantage of individual tasks with a facade interface to them all.

Every task is a OpenInteract2::Manage subclass and is responsible for:

  • Naming itself. The get_name() method returns the task name associated with the class -- so OpenInteract2::Manage::Website::Create has a name of 'create_website', and this is what you'd lookup the task by.

  • Describing itself. A call to get_brief_description() returns a one- or two-sentence description of what the task accomplishes. Additional documentation is in the POD, and you can view that POD separate from that of all other tasks. (It's a normal class after all!)

  • Listing its parameters. The get_parameters() method returns a hashref with parameter names and metadata that describe how to change the task's behavior.

  • Validating its parameters. Any parameter value passed in can be validated. If it is invalid execution is stopped, all transparently to the individual task.

  • Tracking its own status. As the task runs it can generate one or more status messages. These are in a standard format (hashref with specified keys) and the task caller has a single way to retrieve them (call to get_status()).

  • Cleaning up after itself. Tasks should ensure they don't leave any stray database connections open or files scattered around the filesystem.

  • And finally, Running the task!

SUBCLASSING

Now we'll discuss how to create a subclass. When we task about method names below we're always talking about OpenInteract2::Manage unless otherwise indicated.

Mandatory methods

Management tasks must implement:

run_task()

This is where you actually perform the work of your task. You can indicate the status of your task with status hashrefs passed to _add_status() or _add_status_head(). (See STATUS MESSAGES in OpenInteract2::Manage.)

Errors are indicated by throwing an exception -- generally an OpenInteract2::Exception object, but if you want to create your own there is nothing stopping you.

The task manager will set the parameter task_failed to 'yes' if it catches an error from run_task. This allows you to do conditional cleanup in tear_down_task(), discussed below. (For example, if the main task died you probably don't need that directory tree you just created...)

Note that the caller ensures that the current working directory remains the same for the caller, so you can chdir to your heart's content.

get_parameters()

Well, technically you don't have to implement this yourself, you could inherit it. But you really, really should do it yourself, if only to help to poor sap who has to maintain your task.

This method should return a hashref with the keys as parameter names and hashrefs of parameter metadata as the values. The parameter metadata may consist of the following. (Note: 'bool' means that 'yes' equals true while everything else is false.)

description

What this parameter is used for. Strongly advised.

is_required (bool)

Indicates this is a required parameter. Note that all required parameters are automatically submitted for validation. (That doesn't mean you have to validate them, just that they're available for it.)

do_validate (bool)

Tells the managment dispatcher to validate this parameter. You don't need this if 'is_required' is set.

is_boolean (bool)

Indicates this parameter is boolean, or a toggled value.

is_multivalued (bool)

Indicates this parameter may hold multiple values.

default

A default value to use for the parameter, used if the task caller doesn't provide one.

Note that most users of your task will probably call task_parameters() instead of get_parameters() as it provides some additional information. This is also what you will find in the OpenInteract2::Manage documentation.

The parent management task provides _get_source_dir_param() which can be used as necessary:

 sub get_parameters {
     my ( $self ) = @_;
     return {
         source_dir => $self->_get_source_dir_param(),
         ...
     };
}

It specifies that the parameter is required and gives a generic description, plus a default value of the current directory.

Optional methods

init( @extra )

This is called within the new() method. All extra parameters sent to new() are passed to this method, since the main parameters have already been set in the object.

get_name()

Return the task name with which your class is associated. This is how people lookup your task: they don't use the class name, they use the normal name for us humans.. For instance, OpenInteract2::Manage::Website::InstallPackage is associated with 'install_package'.

get_brief_description()

Return a string a sentence or two long describing what the task does.

setup_task()

Sets up the environment required for this task. This might require creating an OpenInteract2::Context, a database connection, or some other action. (Some of these have shortcuts -- see below.)

Note that there may be an abstract subclass of OpenInteract2::Manage that implements common functionality for you here. For instance, OpenInteract2::Manage::Website automatically creates a context here so you don't have to.

If you cannot setup your required environment you should throw an exception with an appropriate message.

tear_down_task()

If your task needs to do any cleanup actions -- closing a database connection, etc. -- it should perform them here.

The task manager will set the parameter task_failed to 'yes' if the main task threw an error. This allows you to do conditional cleanup -- for instance, OpenInteract2::Manage::Website::Create checks this field and if it is set will remove the directories created and all the files copied in the halted process of creating a new website.

validate_param( $param_name, $param_value )

Implement if you'd like to validate one or more paramter values. Note that you should call SUPER as the last command, just in case it has its own validation routines. (More below.)

Parameter Validation

Here's an example where we depend on the validation routine for website_dir from OpenInteract2::Manage:

 sub get_parameters {
     my ( $self ) = @_;
     return {
         website_dir => {
             description => 'a directory',
             is_required => 'yes',
         },
     };
 }
 
 # we're not validating anything ourselves -- no 'validate_param'
 # subroutine defined

Easy enough. Now, say we want to validate a different parameter ourselves:

 sub get_parameters {
     my ( $self ) = @_;
     return {
         game_choice => {
             description => 'Your choice in the game',
             is_required => 'yes',
         },
         ...
     };
 }
  
 sub validate_param {
     my ( $self, $param_name, $param_value ) = @_;
     if ( $param_name eq 'game_choice' ) {
         unless ( $param_value =~ /^(rock|scissors|paper)$/i ) {
             return "Value must be 'rock', 'scissors' or 'paper'";
         }
     }
     return $self->SUPER::validate_param( $param_name, $param_value );
 }

This ensures that the parameter value for 'game_choice' (a) exists and (b) is either 'rock', 'scissors' or 'paper' (case-insensitive). Your run_task() method will never be run unless all the parameter requirements and validation checks are successful.

Status helper methods

These methods should only be used by management tasks themselves, not by the users of those tasks.

Note: All status messages are sent to the observers as a 'status' observation. These are sent in the order received, so the user may be a little confused if you use _add_status_head().

_add_status( \%status, \%status, ... )

Adds status message \%status to those tracked by the object.

_add_status_head( \%status, \%status, ... )

Adds status messages to the head of the list of status messages. This is useful for when your management task comprises several others. You can collect their status messages as your own, then insert an overall status as the initial one seen by the user.

Status helper shortcuts

There are also two shortcuts that you will probably use most often:

_ok( $action, $message, %extra )

Create a passing status message for $action and $message. Any data in %extra will be added to the status message and passed along to the user.

_fail( $action, $message, %extra )

Create a failing status message for $action and $message. Any data in %extra will be added to the status message and passed along to the user.

Notifying Observers

All management tasks are observable. This means anyone can add any number of classes, objects or subroutines that receive observations you post. Notifying observers is simple:

 $self->notify_observers( $type, @extra_info )

What goes into @extra_info depends on the $type. The two types of observations supported right now are 'status' and 'progress'. The 'status' observations are generated automatically when you use _add_status() or _add_status_head() (see above).

Generally 'progress' notifications are accompanied by a simple text message. You may also pass as a third argument a hashref. This hashref gives us room to grow and the observers the ability to differentiate among progress messages. For now, the hashref only supports one key: long. If you're posting a progress notification of a process that will take a long time, set this to 'yes' so the observer can differentiate -- let the user know it will take a while, etc.

 sub run_task {
     my ( $self ) = @_;
     $self->_do_some_simple( 'thing' );
     $self->notify_observers( progress => 'Simple thing complete' );
     $self->_do_some_other( @stuff );
     $self->notify_observers( progress => 'Other stuff complete' );
     $self->notify_observers( progress => 'Preparing complex task',
                              { long => 'yes' } );
     $self->_do_complex_task;
     $self->notify_observers( progress => 'Complex task complete' );
 
     # This fires an implicit observation of type 'status'
     $self->_add_status({ is_ok   => 'yes',
                          message => 'Foobar task ok' });
 }

This is a contrived example -- if your task is very simple (like this) you probably don't need to bother with observations. The notifications generated by the status messages will be more than adequate.

However, if you're looping through a set of packages, or performing a complicated set of operations, it can be very helpful for your users to let them know things are actually happening.

Example

Here is an example of a direct subclass that just creates a file 'hello_world' in the website directory:

 package Openinteract2::Manage::MyTask
 
 use strict;
 use base qw( OpenInteract2::Manage::Website );
 
 sub get_name {
     return 'hello_world';
 }
 
 sub get_brief_description {
     return "Creates a 'hello_world' file in your website directory.";
 }
 
 sub get_parameters {
     my ( $self ) = @_;
     return { website_dir => $self->_get_website_dir_param,
              hello_message => {
                  description => 'Message to write to file',
                  is_required => 'yes',
              },
     };
 }
 
 sub run_task {
     my ( $self ) = @_;
     my $website_dir = $self->param( 'website_dir' );
     $website_dir =~ s|/$||;
     my $filename = File::Spec->catfile( $website_dir, 'hello_world' );
     my %status = ();
     if ( -f $filename ) {
         $status{message} = "Could not create [$filename]: already exists";
         $status{is_ok}   = 'no';
         $self->_add_status( \%status );
         return;
     }
     eval { open( HW, '>', $filename ) || die $! };
     if ( $@ ) {
         $status{message} = "Cannot write to [$filename]: $@";
         $status{is_ok}   = 'no';
     }
     else {
         print HW $self->param( 'hello_message' );
         close( HW );
         $status{is_ok}   = 'yes';
         $status{message} = "File [$filename] created ok";
     }
     $self->_add_status( \%status );
 }
 
 OpenInteract2::Manage->register_factory_type( get_name() => __PACKAGE__ );

 1;

And here is how you would run your task:

 #!/usr/bin/perl
 
 use strict;
 use OpenInteract2::Manage;

 my $task = OpenInteract2::Manage->new( 'hello_world', {
    website_dir => $ENV{OPENINTERACT2}
 });
 my @status = eval { $task->execute };
 if ( $@ ) {
     print "Task failed to run: $@";
 }
 else {
     foreach my $s ( @status ) {
         print "Task OK? $s->{is_ok}\n",
               "$s->{message}\n";
     }
 }

Since all management tasks are auto-discovered by OpenInteract2::Manage at startup, you can also run:

 $ oi2_manage hello_world

And it'll work!

Other subclass helper methods

_setup_context( @params )

Sets up a context given the website directory named in the parameter website_dir. If the 'debug' parameter is true it sets the level of the root log4perl logger to be 'debug'.

COPYRIGHT

Copyright (c) 2003-2004 Chris Winters. All rights reserved.

AUTHORS

Chris Winters <chris@cwinters.com>