From Request Tracker Wiki
Revision as of 15:19, 8 January 2017 by Barton (talk | contribs) (→‎Crontab Entries -- reformat)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


Based on the Modul/Condition UntouchedInHours and the Ideas from TimedNotifications we created a new Condition called UntouchedInBusinessHours.

Compared to UntouchedInHours we calculate within this module based on the Create Time of the Ticket and not on LastUpdated and also only within a definable Business Hours Range.

Problem Scenario

We needed in some Change Request Queues some kind of escalation mixed with notifications based on given business hours model. This escalation should be done in two steps, first escalation should inform head of the queue and raise the ticket priority and second should inform head of IT and raise the priority again.


As solution for this setup we found a mixture of rtcrontool / cron, UntouchedInHours and TimedNotifications.

Crontab Entries

   */15 7-15 * * * /opt/rt3/bin/rt-crontool
   --search RT::Search::FromSQL
   --search-arg "Queue = 'ChangeManagement' AND Status = 'new' AND Owner = 'Nobody' AND CF.Escalation = 'InTime'"
   --condition RT::Condition::UntouchedInBusinessHours
   --condition-arg 8:7:15:0
   --action RT::Action::RecordComment
   --template 'esc 1 - 8Std FirstContact'

Queue Preparation

To make sure to escalate in the correct order, we added to each change queue a CustomField and a Custom Scrip


For each Change Queue we added a CustomField Escalation which will be filled at ticket creation with the Value "InTime" and updated after first escalation with the Value "FirstEscalation" and after second escalation with the value "SecondEscalation" This values are used inside the search as additional Search Argument.

Custom Scrip

We added to all change queues a simple scrip which set the CF Escalation at ticket create time to "InTime"

Scrip: 001_OnCreateSetEscalationCF

Condition: OnCreate Action: UserDefined Template: Blank

Custom action preparation code:

return 1;

Custom action cleanup code:

$self->TicketObj->AddCustomFieldValue(Field => 'Escalation', Value => 'InTime', RecordTransaction => 0); return 1;


Based on the ideas from we create some templates for the escalation, which will sent out a mail and raise the priority by 10 and update the CustomField "Escalation"

Template: esc 1 - 8Std FirstContact

Subject: Escalation 1 for Ticket #: {$Ticket->id} - {$Ticket->Subject()}!

RT-Send-Cc: head1@company.com
 this is an automated Escalation Mail from the Request Tracker.
 The following Change Request is older than 8 hours without being attended to.
 Ticket Id: {$Ticket->id}
 Queue name: {$Ticket->QueueObj->Name}
 Escalation: {$Ticket->AddCustomFieldValue(Field =>'Escalation', Value => 'FirstEscalation',RecordTransaction => 0)}
 Priority: {$Ticket->SetPriority($Ticket->Priority + 10)}
 Subject: {$Ticket->Subject()}
 Requestor: {$Ticket->RequestorAddresses}
 Ticket URL: {$RT::WebURL}Ticket/Display.html?id={$Ticket->id}
 Please take care of the Change Request immediately.
 Thank you


After all the preparation, we implemented our new Condition to RT under RT_HOME/local/lib/RT/Condition called UntouchedInBusinessHours.pm

You can download the file here: http://www.brumm.me/rt/UntouchInBusinessHours.pm

package RT::Condition::UntouchedInBusinessHours;

require RT::Condition::Generic;
  use strict;
  use Time::Local;
  use vars qw/@ISA/;
  @ISA = qw(RT::Condition::Generic);
  =head1 NAME
  L<RT::Condition::UntouchedInBusinessHours> - Check for untouched Tickets within business hours
  =head1 SYNOPSIS
  =head2 CLI
  	--search RT::Search::ModuleName
  	--search-arg "The Search Argument"
  	--condition RT::Condition::UntouchedInBusinessHours
  	--condition-arg "The Condition Argument"
  	--action RT::Action:ActionModule
  	--template 'Template Name or ID'
  L<RT::Condition::UntouchedInBusinessHours> is a RT Condition which will check
  for untouched Tickets within business hours.
  Untouched means in this case really untouched from time of ticket creation.
  =head2 Condition Argument
  The L<RT::Condition::UntouchedInBusinessHours> need exactly 4 arguments to work.
  Each part of the argument is separated by colons.
  	--condition RT::Condition::UntouchedInBusinessHours
  	--condition-arg 1:7:15:0
  	1 is the time in hours for escalation
  	7 is the start hour of the working day
  	15 is the end of of the working day
  	0 is a 5 day week from monday to friday
  	1 is a 7 day week from monday to sunday
  =head2 Example cron call
  		--search RT::Search::FromSQL
  		--search-arg "Queue = 'General' AND ( Status = 'new' ) AND Owner = 'Nobody'"
  		--condition RT::Condition::UntouchedInBusinessHours
  		--condition-arg 1:7:15:0
  		--action RT::Action::RecordComment
  		--template 'Unowned tickets'
  =head1 Limitations
  At this moment only week/weekends from mo-fr/sa-su are supported. If you have different scenario
  (like arabic countries) you have to change inside the source code ($wd =~ m/(Sun|Sat)/io)
  Also no public holidays are excluded.
  =head1 NOTES
  =head2 Reporting
  Send reports to L</AUTHOR> or to the RT mailing lists.
  =head2 AUTHOR
      Torsten Brumm <torsten.brumm@googlemail.com>
  =head2 COPYRIGHT
  This program is free software; you can redistribute
  it and/or modify it under the same terms as Perl itself.
  The full text of the license can be found in the
  Perl distribution.
  my ($est, $tc, $bsh, $beh, $te, $we);
  my ($sec,$min,$hour,$mday,$mon,$year,$wd);
  my $ActualDate;
  my $ticketObj;
  my $tickid;
  my $EscalationDate;
  sub IsApplicable {
  	my $self = shift;
  	$ticketObj = $self->TicketObj;
  	$tickid = $ticketObj->Id;
  	$tc = $ticketObj->CreatedObj->Unix;
  	($est, $bsh, $beh, $we) = split(/:/, $self->Argument);
  	$ActualDate = local_actual_date_sec(RT::Date->new($RT::SystemUser));
  	$EscalationDate = escalate(normalize($tc, $bsh, $beh, $we), $bsh, $beh, $est, $we);
  	if ( $EscalationDate <= $ActualDate ) {
  		return 1;
  	return undef;
  sub local_actual_date_sec {
         my $self = shift;
         my $Ausgabe_local_actual_date_sec = $self->Unix;
         return ($self->Unix);
  sub escalate {
  	my ($tc, $bs, $be, $et, $we) = @_;
  	my ($rs, $es);
  	my $bar = 60*60;
  	$rs = $es = $et*$bar;
  	my $elapsed = 0;
  	my ($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($tc);
  	my $beepo = timegm(0,0,$be,$mday,$mon,$year);
  	my ($besec,$bemin,$behour,$bemday,$bemon,$beyear,$bewd) = fromepoch($beepo);
  	my $foo = $beepo-$tc;
  	if ($es < $foo) { # escalate on the same day within BH
  		return $tc+$es;
  	if ($hour < $behour) {
  		$elapsed += $foo;
  		$rs -= $foo;
  		($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($tc+86400);
  		$tc = timegm(0,0,$bs,$mday,$mon,$year);
  		$tc = normalize($tc, $bs, $be, $we);
  		($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($tc);
  	while(int($rs / ($bar*($be -$bs)))) {
  		$elapsed += ($bar * ($be -$bs));
  		$rs -= ($bar * ($be - $bs));
  		($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($tc+86400);
  		$tc = timegm(0,0,$bs,$mday,$mon,$year);
  		$tc = normalize($tc, $bs, $be, $we);
  		($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($tc);
  	$tc = normalize($tc+$rs, $bs, $be);
  	return $tc;
  sub normalize {
  	my ($time, $bs, $be, $we) = @_;
  	my ($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($time);
  	my $bens = timegm(0,0,$bs,$mday,$mon,$year);
  	my $bend = timegm(0,0,$be,$mday,$mon,$year);
  	if ($time >= $bend) {
  		($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($time+86400);
  		$time = $bens = timegm(0,0,$bs,$mday,$mon,$year);
  		$bend = timegm(0,0,$be,$mday,$mon,$year);
  	if ($time < $bens) {
  		$time = timegm(0,0,$bs,$mday,$mon,$year);
  	if (! $we) {
  		while ($wd =~ m/(Sun|Sat)/io) {
  			($sec,$min,$hour,$mday,$mon,$year,$wd) = fromepoch($time + 86400);
  			$time = timegm(0,0,$bs,$mday,$mon,$year);
  	return $time;
  sub fromepoch {
  	my ($t) = @_;
  	my @time = gmtime($t);
  	my $ss = ($time[0]<10) ? "0".$time[0] : $time[0];
  	my $mm = ($time[1]<10) ? "0".$time[1] : $time[1];
  	my $hh = ($time[2]<10) ? "0".$time[2] : $time[2];
  	my $day = $time[3];
  	my $month = ($time[4] < 10) ? "0".($time[4]) : $time[4];
  	my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
  	my $year = $time[5] + 1900;
  	my $offset = timelocal(localtime) - timelocal(gmtime);
  	my $sign ="+";
  	if ($offset < 0) {
  		$sign ="-";
  		$offset *= -1;
  	my $offseth = int($offset/3600);
  	my $offsetm = int(($offset - $offseth*3600)/60);
  	my $tz = sprintf ("%s%0.2d%0.2d", $sign, $offseth, $offsetm);
  	return ($ss, $mm, $hh, $day, $month, $year, $wday);
  eval "require RT::Condition::UntouchedInBusinessHours_Vendor";
  die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInBusinessHours_Vendor.pm});
  eval "require RT::Condition::UntouchedInBusinessHours_Local";
  die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInBusinessHours_Local.pm});