[rt-users] FAQ: Creating ScripConditions
Bruce Campbell
bruce_campbell at ripe.net
Tue Feb 5 13:35:47 EST 2002
This documents how to create a basic ScripCondition yourself. It uses a
comparitive minimum of features, and gives a rough overview of what
features you can use. *Most* of what is described is also applicable to
ScripActions, with the exception that a ScripAction has two invoked
methods, 'Prepare()' and 'Commit()'.
Overview:
For a Scrip to be executed, a number of factors determine RT's choice,
before RT even loads the code behind a Scrip.
Firstly, the Scrip must either be Global, or specific to a Queue.
Secondly, the Transaction being created must be one of those described in
the ScripCondition's ApplicableTransType field.
Thirdly, the ScripCondition's 'IsApplicable()' method must return true.
This is what we're going to cover.
If the ScripCondition's IsApplicable returns true, RT then proceeds to
load the appropriate ScripAction, Prepare() it, and if that was
successful, Commit() it. ( The dry bit that does this is in
lib/RT/Transaction.pm - Create ).
All of the above (except for the public methods() ) are described in the
SQL tables concerning Scrips. Lets have a look there:
SQL references; Scrips;
In the Scrips table, RT looks at the following:
mysql> select * from Scrips;
+----+----------------+-------------+-------------------+-------+----------+---------+---------+---------------+-------------+
| id | ScripCondition | ScripAction | Stage | Queue | Template | Creator | Created | LastUpdatedBy | LastUpdated |
+----+----------------+-------------+-------------------+-------+----------+---------+---------+---------------+-------------+
| 1 | 1 | 1 | TransactionCreate | 2 | 1 | NULL | NULL | NULL | NULL |
[snip]
Firstly, we have the id field of the Scrip. Secondly, we have the ids for
the ScripCondition and ScripAction. These are used as indexes in the
ScripConditions and ScripActions tables.
Next, we have the stage of the Scrip. Currently RT has only one Stage,
being the TransactionCreate, but has the possibility of being extended.
Then we have the Queue identifier (again, used as an index in the Queues
table) and the Template identifier (index into Templates). The remaining
fields are merly informational, and we can ignore them in this context.
SQL References; ScripConditions:
The next step is to look at the ScripCondition. This can be done
selecting the same identifier from the ScripConditions table as is
mentioned in the Condition (see the Scrips table):
mysql> select * from ScripConditions where id = '1';
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+
| id | Name | Description | ExecModule | Argument | ApplicableTransTypes | Creator | Created | LastUpdatedBy | LastUpdated |
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+
| 1 | OnCreate | When a ticket is created | AnyTransaction | NULL | Create | 1 | 2001-12-23 21:25:18 | 1 | 2001-12-23 21:25:18 |
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+
Again, we have the identifier of the ScripCondition. We also have a
descriptive Name, and an expanded Description. Next, we have an odd field
named 'ExecModule', being the real magic behind Scrips, and covered
further on. Next we have an optional Argument, and a list of applicable
Transactions types. The remaining fields are again, informational and not
covered here.
In this case, RT will load this Scrip when a Transaction is Created, hence
the name 'OnCreate' (descriptive only) and 'Create' in
ApplicableTransTypes (used by RT).
Loading a ScripCondition:
Now that we've read the SQL table to know what the ScripCondition is and
we're in an Applicable Transaction Type, the next task is to load it.
This is done via the LoadCondition() method in lib/RT/ScripCondition.pm,
aka:
$self->ExecModule =~ /^(\w+)$/; # match the word
my $module = $1; # store the word
my $type = "RT::Condition::" . $module; # Prepare the word
# Use the word
eval "require $type" || die "Require of $type failed.\n$@\n";
The above retrieves the name of the ExecModule from the database
('AnyTransaction'), and tries to 'require' it as
RT::Condition::AnyTransaction. Replacing the '::'s with '/' indicates
that RT is reading in lib/RT/Condition/AnyTransaction.pm .
( Note that RT is also doing some other tricks to ensure that if RT is
unable to load a ScripCondition, RT doesn't completely die. Having bad
code during testing is ok and won't badly affect your installation. )
Examining a ScripCondition:
Looking at our example Condition (lib/RT/Condition/AnyTransaction.pm), we
can see that its pretty simple.
The essential part of it is the following:
# Declare what we are:
package RT::Condition::AnyTransaction;
# Read in the Generic RT Goodness
require RT::Condition::Generic;
# Load the Generic RT Goodness into our namespace
@ISA = qw(RT::Condition::Generic);
The above lines (note that I've put in extra comments for clarity) do most
of the footwork behind a ScripCondition. We'll cover some of what
RT::Condition::Generic does for you later.
Next in the code is the public method, 'IsApplicable()'. Back in
lib/RT/Transaction.pm, you can see that RT calls the 'IsApplicable()'
method to see if RT should execute the ScripAction. In AnyTransaction,
IsApplicable is extremely simple:
sub IsApplicable {
# Get our OO $self
my $self = shift;
# This routine returns true in any case.
return(1);
}
Writing our Own ScripCondition:
In this example, we're going to create our own ScripCondition. As this is
a step-by-step example, we're not going to do anything complicated, but we
are going to do something useful.
In a typical RT install, a Scrip is supplied that reads something like:
OnCreate AutoreplyToRequestors with template Autoreply
This gives an Autoreply each time that a transaction is created, normally
via Email. Wouldn't it be useful to give a different AutoReply if a
request came in outside Office Hours?
So, we're going to create a ScripCondition that checks what time it is,
and we'll eventually end up with:
OnCreateOfficeHours AutoreplyToRequestors with template Autoreply-hours
OnCreateOutsideHours AutoreplyToRequestors with template Autoreply-outside
Sounds easy, right?
The Right Place to Put ScripConditions:
The correct place to put ScripConditions is in the 'Condition'
subdirectory of your RT's library, ie
'lib/RT/Condition/MyScripCondition.pm'. Personally, I'm a great believer
of maintaining a seperate place for localisations, and I have the
following setup:
/home/rt2/etc/site_scrips/Condition/MyLocalCondition.pm
/home/rt2/lib/RT/Condition/MyLocalCondition.pm --symlink-to-->
/home/rt2/etc/site_scrips/Condition/MyLocalCondition.pm
( This is so that when I upgrade, I just have to recreate the symbolic
links, and not my localisations )
Everyone has their own method, however RT must be able to load
MyLocalCondition.pm from 'lib/RT/Condition/MyLocalCondition.pm'.
Example ScripCondition; Crontab.pm:
Bearing the above in mind, we're going to create our own ScripCondition
named 'Crontab.pm'. Although we're going to appear to end up with 2
ScripConditions, we only need one ExecModule (RT::Condition::Crontab.pm).
Later, we'll be creating two ScripConditions that will have the same
ExecModule, but will differ in their Names, Descriptions and Arguments.
( Code Re-use is Good for you )
Firstly, we set up ourselves:
# ScripCondition: Crontab.pm
# IsApplicable returns true if the current time is within the
# range specified by the Argument, undef otherwise.
# Bruce Campbell says to always give yourself Credit, and
# to use RCS (man rcs):
# $Id$
# Who are we?
package RT::Condition::Crontab;
# Bring all the RT Goodness into our lives.
require RT::Condition::Generic;
# and our namespace
@ISA = qw(RT::Condition::Generic);
# We're going to use this later.
use Set::Crontab;
Now we've got our preparations out of the way, we can do some work.
Remember that the only public method is the 'IsApplicable()' one:
# Override RT::Condition::Generic's IsApplicable method:
sub IsApplicable {
# Get our OO $self.
my $self = shift;
# Prepare our default return value
my $retval = undef;
Now what? We need to get the current time. We'll use RT::Date for this.
my $cur_time = new RT::Date( $RT::Nobody );
$cur_time->SetToNow();
Next, we need to find out the time ranges that we're valid for. For this,
we'll use the Argument that will be stored in the ScripConditions table
later. We'll use the standard crontab specifiers as they're simple, and a
'man crontab' will give the usage.
To allow for multiple time ranges in one ScripCondition, we'll use the ':'
character, so:
my @time_ranges = split( ':', $self->Argument );
Huh? Where do we get the Argument from? Its imported from
RT::Condition::Generic, as a helper method for getting values from
SQL concerning the current ScripCondition, Transaction (etc).
Next, we need to see if 'Now' is within any of the ranges given above.
For this, we need to use the nice Set::Crontab module from CPAN. Do a
"perl -MCPAN -e'install Set::Crontab'" to install this.
# Prepare 'now'.
# We'll use localtime on the Unix() method of RT::Date
my @nowlist = localtime( $cur_time->Unix );
# Build up a compare list.
my @wantlist = ();
# Names are not supported in Set::Crontab
$wantlist[0] = $nowlist[1]; # minute 0-59
$wantlist[1] = $nowlist[2]; # hour 0-23
$wantlist[2] = $nowlist[3]; # day of month 1-31
$wantlist[3] = $nowlist[4] + 1; # month 1-12
$wantlist[4] = $nowlist[6]; # day of week 0-7
We also need to set up the known ranges for cron
# Known ranges
my @known_ranges = (
[0..59], # minute 0-59
[0..23], # hour 0-23
[1..31], # day of month 1-31
[1..12], # month 1-12
[0..7], # day of week 0-7
);
We loop through the various @time_ranges that we've found.
foreach my $this_range ( @time_ranges ){
# Remove leading or trailing space (we split() on that later)
$this_range =~ s/^\s*//g;
$this_range =~ s/\s*$//g;
# How many of the fields have we matched so far?
my $matched = 0;
# Split apart the fields based on whitespace.
my @this_list = split( /\s+/, $this_range );
And we also loop through each field in the time range.
# See if each field is in within range.
my $loop = 0;
while( ( $loop < scalar @wantlist ) &&
( $loop < scalar @this_list ) ){
my $tst = Set::Crontab->new(
$this_list[$loop],
$known_ranges[$loop] );
if( $tst->contains(
$wantlist[$loop] ){
$matched++;
}
Always remember to keep moving forward.
# Increment the loop
$loop++;
}
Now we check to see whether we matched them all. If we didn't, its
obviously not this time range.
# Did we match them all?
if( ( $matched == $loop ) &&
( $loop == scalar @wantlist ) ){
# We did. Set our return value.
$retval++;
}
}
Finally, we return with our results.
# retval is undef when declared, and only incremented
# if a time value matches.
return( $retval );
}
The very last thing we need in Crontab.pm is to return true for the
'require'. So, we put:
1;
At this point, it might be best to grab the working version from:
http://www.fsck.com/pub/contrib/2.0/
( Note, I'm not sure where Jesse is going to put it )
Inserting a ScripCondition into the Database; Template:
Now that we have our Code to execute the ScripCondition, we need to Create
the Condition in the SQL database. With RT2.0.x, there is an interface
for Creating Scrips, but no easy method of calling it from the Web or
command line interfaces.
Instead, we're going to write a perl program to do it for us. Save this
as 'insert_condition_Crontab_Template.pl':
#!/usr/bin/perl -w
Remember to change these two variables as needed.
# Where is our RT library?
use lib "!!CHANGE_ME_TO_RT_LIB_DIRECTORY!!";
# Where is our configuration?
use lib "!!CHANGE_ME_TO_RT_ETC_DIRECTORY!!";
# Get our base configuration (config.pm)
use config;
# pretty errors.
use Carp;
# use a few more things related to RT
use RT::Handle;
use RT::User;
use RT::CurrentUser;
# Connect to the Database (low level)
$RT::Handle = new RT::Handle( $RT::DatabaseType );
$RT::Handle->Connect;
#Put together a current user object so we can create a User object
my $CurrentUser = new RT::CurrentUser();
#now that we bootstrapped that little bit, we can use the standard RT cli
# helpers to do what we need
use RT::Interface::CLI qw(CleanEnv LoadConfig DBConnect
GetCurrentUser GetMessageContent);
#Clean out all the nasties from the environment
CleanEnv();
#Load etc/config.pm and drop privs
LoadConfig();
#Connect to the database and get RT::SystemUser and RT::Nobody loaded
DBConnect();
# Load the System User
$CurrentUser->LoadByName('RT_System');
At this point, RT has connected to the database as the System User. From
here, we need to define what we put into the database:
# The Argument for Crontab.pm is a ':' seperated list of
# crontab-like entries. See 'man crontab' (5) for a full
# description of crontab configuration.
my @ScripConditions = (
{
Name => 'OnCreateHours',
Description => 'Matches at any time',
ApplicableTransTypes => 'Create',
Argument => '* * * * *',
ExecModule => 'Crontab',
},
);
And finally, we need to put it into the database:
# The above is the user changable stuff. Now create a Scrip with
# the supplied data
print "Creating ScripConditions...";
use RT::ScripCondition;
for $item (@ScripConditions) {
my $new_entry = new RT::ScripCondition($CurrentUser);
my $return = $new_entry->Create(%$item);
print $return.".";
}
print "done.\n";
$RT::Handle->Disconnect();
1;
Save that file as 'insert_condition_Crontab_Template.pl' (I like to keep it
these in /home/rt2/etc/site_scrips/notes/ myself, but you may have
different ideas of course), and proceed to the next step.
This should also be available from the same directory on www.fsck.com that
you found Crontab.pm.
Inserting a ScripCondition into the Database; Specifics:
Now that we have a Template for creating a ScripCondition, we're going to
use this to create two ScripConditions, 'OnCreateOfficeHours' and
'OnCreateOutsideHours' as suggested earlier.
To do this, make two copies of the Template as
'insert_condition_OnCreateOfficeHours.pl' and
'insert_condition_OnCreateOutsideHours.pl'. Note that the names of the
files is arbitary, but simply helps to remember what you're dealing with.
Within the OfficeHours one, replace the Name, Description and Argument as
follows:
Name => 'OnCreateOfficeHours',
Description => 'Fires when a Create is done within Office \
Hours',
Argument => '* 9-16 * * 1-5',
Change as appropriate if your office hours are outside the sterotypical
9-5. For example, the following describes 8:45am to 17:34pm:
Argument => '45-59 8 * * 1-5:* 9-16 * * 1-5:0-15 17 * * 1-5',
Within the OutsideHours one, replace the Name, Description and Argument as
follows:
Name => 'OnCreateOutsideHours',
Description => 'Fires when a Create is done outside Office \
Hours',
Argument => '* 0-8 * * 1-5:* 17-23 * * 1-5:* * * * 0,6',
All that you need to do now is to run these perl programs (but not the
Template one), and you've created two new ScripConditions.
Using your New ScripConditions:
Using these ScripConditions is as easy as using any normal ScripCondition.
Firstly, you need to create two new Templates, 'Autoreply-hours' and
'Autoreply-outside' with appropriate texts, ie:
Autoreply-hours:
Greetings,
This message has been automatically generated in response
to the creation of a trouble ticket regarding:
"{$Ticket->Subject()}" .
We are on duty now, so you should receive a reply within
the next hour.
(etc)
Autoreply-outside
Greetings,
This message has been automatically generated in response
to the creation of a trouble ticket regarding:
"{$Ticket->Subject()}" .
Our office hours are between 9am and 5pm weekdays, and
our local timezone is CET (GMT+0100). We will answer your
query on the next working day.
(etc)
Finally, you need to create the Scrips using the Templates and the
Conditions that we've written, as:
OnCreateOfficeHours AutoreplyToRequestors with template Autoreply-hours
OnCreateOutsideHours AutoreplyToRequestors with template Autoreply-outside
Thats all there is to it. Really.
Other things from RT::Condition::Generic:
The Generic module for ScripConditions handles all of the setup for that you
need in order to create your own ScripCondition. By 'require'ing it, you
can just override what you need, normally just the 'IsApplicable' method.
The useful methods in Generic are the Argument() which we've seen above,
TicketObj() which gives you access to the current Ticket object, and
TransactionObj() which gives you access to the current Transaction object.
Both of these can be referenced from within
RT::Condition::MyLocalCondition as $self->TicketObj or
$self->TransactionObj. Because they are references to Ticket and
Transaction objects, you can do neat things like:
if( $self->TransactionObj->CreatorObj->EmailAddress() =~ /magic/ ){
At this point, the best method to learn is to use some examples. Plenty
of examples can be found on the Rt distribution site, at
http://www.fsck.com/pub/rt/contrib/2.0 . As a guideline for understanding
the code, always remember that '$self' refers to something within
ourselves (man perlboot).
If you see a reference that you don't get, search through the other files
that have been 'require'd or 'use'd until you find it. Sometimes you will
have to go back into the code to find out where something is defined.
For example, RT::Condition::Generic uses $self->{'TransactionObj'}. This
gets loaded by RT::ScripCondition->LoadCondition(), which is handed the
Transaction Object when called. The only place LoadCondition() is called
is within RT::Transaction->Create(), which passes a copy of itself.
phew. (theres no easy way of describing it. Use grep over the whole
lib/RT tree if you have to)
Regards,
--
Bruce Campbell RIPE
Systems/Network Engineer NCC
www.ripe.net - PGP562C8B1B Operations
More information about the rt-users
mailing list