b6a5b141cf1b03d60ee6868c3b92d09d85ae0f52
[usit-rt.git] / bin / rt-crontool
1 #!/usr/bin/perl
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
8 #
9 # (Except where explicitly superseded by other copyright notices)
10 #
11 #
12 # LICENSE:
13 #
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18 #
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29 #
30 #
31 # CONTRIBUTION SUBMISSION POLICY:
32 #
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38 #
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47 #
48 # END BPS TAGGED BLOCK }}}
49 use strict;
50 use Carp;
51
52 use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
53 use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
54 use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
55
56 # fix lib paths, some may be relative
57 BEGIN {
58     require File::Spec;
59     my @libs = ("lib", "local/lib");
60     my $bin_path;
61
62     for my $lib (@libs) {
63         unless ( File::Spec->file_name_is_absolute($lib) ) {
64             unless ($bin_path) {
65                 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
66                     $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
67                 }
68                 else {
69                     require FindBin;
70                     no warnings "once";
71                     $bin_path = $FindBin::Bin;
72                 }
73             }
74             $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
75         }
76         unshift @INC, $lib;
77     }
78
79 }
80
81 use RT;
82
83 use Getopt::Long;
84
85 use RT::Interface::CLI qw(CleanEnv GetCurrentUser GetMessageContent loc);
86
87 #Clean out all the nasties from the environment
88 CleanEnv();
89
90 my ( $search, $condition, $action, $search_arg, $condition_arg, $action_arg,
91      $template, $template_id, $transaction, $transaction_type, $help, $log, $verbose );
92 GetOptions(
93     "search=s"           => \$search,
94     "search-arg=s"       => \$search_arg,
95     "condition=s"        => \$condition,
96     "condition-arg=s"    => \$condition_arg,
97     "action-arg=s"       => \$action_arg,
98     "action=s"           => \$action,
99     "template=s"         => \$template,
100     "template-id=s"      => \$template_id,
101     "transaction=s"      => \$transaction,
102     "transaction-type=s" => \$transaction_type,
103     "log=s"              => \$log,
104     "verbose|v"          => \$verbose,
105     "help"               => \$help,
106 );
107
108 # Load the config file
109 RT::LoadConfig();
110
111 # adjust logging to the screen according to options
112 RT->Config->Set( LogToScreen => $log ) if $log;
113
114 #Connect to the database and get RT::SystemUser and RT::Nobody loaded
115 RT::Init();
116
117 require RT::Tickets;
118 require RT::Template;
119
120 #Get the current user all loaded
121 my $CurrentUser = GetCurrentUser();
122
123 # show help even if there is no current user
124 help() if $help;
125
126 unless ( $CurrentUser->Id ) {
127     print loc("No RT user found. Please consult your RT administrator.");
128     exit(1);
129 }
130
131 help() unless $search && $action;
132
133 $transaction = lc( $transaction||'' );
134 if ( $transaction && $transaction !~ /^(first|all|last)$/i ) {
135     print STDERR loc("--transaction argument could be only 'first', 'last' or 'all'");
136     exit 1;
137 }
138
139 if ( $template && $template_id ) {
140     print STDERR loc("--template-id is deprecated argument and can not be used with --template");
141     exit 1;
142 }
143 elsif ( $template_id ) {
144 # don't warn
145     $template = $template_id;
146 }
147
148 # We _must_ have a search object
149 load_module($search);
150 load_module($action)    if ($action);
151 load_module($condition) if ($condition);
152
153 my $void_scrip = RT::Scrip->new( $CurrentUser );
154 my $void_scrip_action = RT::ScripAction->new( $CurrentUser );
155
156 #At the appointed time:
157
158 #find a bunch of tickets
159 my $tickets = RT::Tickets->new($CurrentUser);
160 my $search  = $search->new(
161     TicketsObj  => $tickets,
162     Argument    => $search_arg,
163     CurrentUser => $CurrentUser
164 );
165
166 $search->Prepare();
167
168 # TicketsFound is an RT::Tickets object
169 my $tickets = $search->TicketsObj;
170
171 #for each ticket we've found
172 while ( my $ticket = $tickets->Next() ) {
173     print $ticket->Id() . ":\n" if ($verbose);
174
175     my $template_obj = get_template( $ticket );
176
177     if ( $transaction ) {
178         my $txns = get_transactions($ticket);
179         my $found = 0;
180         while ( my $txn = $txns->Next ) {
181             print "\t".loc("Using transaction #[_1]...", $txn->id)."\n"
182                 if $verbose;
183             process($ticket, $txn, $template_obj);
184             $found = 1;
185         }
186         print "\t".loc("Couldn't find suitable transaction, skipping")."\n"
187             if $verbose && !$found;
188     } else {
189         print "\t".loc("Processing without transaction, some conditions and actions may fail. Consider using --transaction argument")."\n"
190             if $verbose;
191
192         process($ticket, undef, $template_obj);
193     }
194 }
195
196 sub process {
197     my $ticket = shift;
198     my $transaction = shift;
199     my $template_obj = shift;
200
201     # perform some more advanced check
202     if ($condition) {
203         my $condition_obj = $condition->new(
204             TransactionObj => $transaction,
205             TicketObj      => $ticket,
206             ScripObj       => $void_scrip,
207             TemplateObj    => $template_obj,
208             Argument       => $condition_arg,
209             CurrentUser    => $CurrentUser,
210         );
211
212         # if the condition doesn't apply, get out of here
213
214         return unless $condition_obj->IsApplicable;
215         print "\t".loc("Condition matches...")."\n" if $verbose;
216     }
217
218     #prepare our action
219     my $action_obj = $action->new(
220         TicketObj      => $ticket,
221         TransactionObj => $transaction,
222         TemplateObj    => $template_obj,
223         Argument       => $action_arg,
224         ScripObj       => $void_scrip,
225         ScripActionObj => $void_scrip_action,
226         CurrentUser    => $CurrentUser,
227     );
228
229     #if our preparation, move onto the next ticket
230     return unless $action_obj->Prepare;
231     print "\t".loc("Action prepared...")."\n" if $verbose;
232
233     #commit our action.
234     return unless $action_obj->Commit;
235     print "\t".loc("Action committed.")."\n" if $verbose;
236 }
237
238 # =head2 get_transactions
239
240 # Takes ticket and returns L<RT::Transactions> object with transactions
241 # of the ticket according to command line arguments C<--transaction>
242 # and <--transaction-type>.
243
244 # =cut
245
246 sub get_transactions {
247     my $ticket = shift;
248     my $txns = $ticket->Transactions;
249     my $order = $transaction eq 'last'? 'DESC': 'ASC';
250     $txns->OrderByCols(
251         { FIELD => 'Created', ORDER => $order },
252         { FIELD => 'id', ORDER => $order },
253     );
254     if ( $transaction_type ) {
255         $transaction_type =~ s/^\s+//;
256         $transaction_type =~ s/\s+$//;
257         foreach my $type ( split /\s*,\s*/, $transaction_type ) {
258             $txns->Limit( FIELD => 'Type', VALUE => $type, ENTRYAGGREGATOR => 'OR' );
259         }
260     }
261     $txns->RowsPerPage(1) unless $transaction eq 'all';
262     return $txns;
263 }
264
265 # =head2 get_template
266
267 # Takes a ticket and returns a template according to command line options.
268
269 # =cut
270
271 { my $cache = undef;
272 sub get_template {
273     my $ticket = shift;
274     return undef unless $template;
275
276     unless ( $template =~ /\D/ ) {
277         # by id
278         return $cache if $cache;
279
280         my $cache = RT::Template->new( RT->SystemUser );
281         $cache->Load( $template );
282         die "Failed to load template '$template'"
283             unless $cache->id;
284         return $cache;
285     }
286
287     my $queue = $ticket->Queue;
288     return $cache->{ $queue } if $cache->{ $queue };
289
290     my $res = RT::Template->new( RT->SystemUser );
291     $res->LoadQueueTemplate( Queue => $queue, Name => $template );
292     unless ( $res->id ) {
293         $res->LoadGlobalTemplate( $template );
294         die "Failed to load template '$template', either for queue #$queue or global"
295             unless $res->id;
296     }
297     return $cache->{ $queue } = $res;
298 } }
299
300
301 # =head2 load_module
302
303 # Loads a perl module, dying nicely if it can't find it.
304
305 # =cut
306
307 sub load_module {
308     my $modname = shift;
309     eval "require $modname";
310     if ($@) {
311         die loc( "Failed to load module [_1]. ([_2])", $modname, $@ );
312     }
313
314 }
315
316
317
318 # =head2 loc LIST
319
320 # Localize this string, with the current user's currentuser object
321
322 # =cut
323
324 sub loc {
325     $CurrentUser->loc(@_);
326 }
327
328
329 sub help {
330
331     print loc( "[_1] is a tool to act on tickets from an external scheduling tool, such as cron.", $0 )
332       . "\n";
333     print loc("It takes several arguments:") . "\n\n";
334
335     print "     "
336       . loc( "[_1] - Specify the search module you want to use", "--search" )
337       . "\n";
338     print "     "
339       . loc( "[_1] - An argument to pass to [_2]", "--search-arg", "--search" )
340       . "\n";
341
342     print "     "
343       . loc( "[_1] - Specify the condition module you want to use", "--condition" )
344       . "\n";
345     print "     "
346       . loc( "[_1] - An argument to pass to [_2]", "--condition-arg", "--condition" )
347       . "\n";
348     print "     "
349       . loc( "[_1] - Specify the action module you want to use", "--action" )
350       . "\n";
351     print "     "
352       . loc( "[_1] - An argument to pass to [_2]", "--action-arg", "--action" )
353       . "\n";
354     print "     "
355       . loc( "[_1] - Specify name or id of template(s) you want to use", "--template" )
356       . "\n";
357     print "     "
358       . loc( "[_1] - Specify if you want to use either 'first', 'last' or 'all' transactions", "--transaction" )
359       . "\n";
360     print "     "
361       . loc( "[_1] - Specify the comma separated list of transactions' types you want to use", "--transaction-type" )
362       . "\n";
363     print "     "
364       . loc( "[_1] - Adjust LogToScreen config option", "--log" ) . "\n";
365     print "     "
366       . loc( "[_1] - Output status updates to STDOUT", "--verbose" ) . "\n";
367     print "\n";
368     print "\n";
369     print loc("Security:")."\n";
370     print loc("This tool allows the user to run arbitrary perl modules from within RT.")." ". 
371         loc("If this tool were setgid, a hostile local user could use this tool to gain administrative access to RT.")." ".
372         loc("It is incredibly important that nonprivileged users not be allowed to run this tool."). " " . 
373         loc("It is suggested that you create a non-privileged unix user with the correct group membership and RT access to run this tool.")."\n";
374     print "\n";
375     print loc("Example:");
376     print "\n";
377     print " "
378       . loc( "The following command will find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:"
379       )
380       . "\n\n";
381
382     print " bin/rt-crontool \\\n";
383     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
384     print "  --condition RT::Condition::Overdue \\\n";
385     print "  --action RT::Action::SetPriority --action-arg 99 \\\n";
386     print "  --verbose\n";
387
388     print "\n";
389     print loc("Escalate tickets"). "\n";
390     print " bin/rt-crontool \\\n";
391     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
392     print"  --action RT::Action::EscalatePriority\n";
393  
394  
395  
396
397
398
399     exit(0);
400 }
401
402 __END__
403
404 =head1 NAME
405
406 rt-crontool - a tool to act on tickets from an external scheduling tool
407
408 =head1 SYNOPSIS
409
410     # find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:
411     rt-crontool \
412       --search RT::Search::ActiveTicketsInQueue  --search-arg general \
413       --condition RT::Condition::Overdue \
414       --action RT::Action::SetPriority --action-arg 99 \
415       --verbose
416
417     # Escalate tickets
418       rt-crontool \
419         --search RT::Search::ActiveTicketsInQueue  --search-arg general \
420         --action RT::Action::EscalatePriority
421
422 =head1 DESCRIPTION
423
424 This script is a tool to act on tickets from an external scheduling tool, such
425 as cron.
426
427 Security:
428
429 This tool allows the user to run arbitrary perl modules from within RT. If
430 this tool were setgid, a hostile local user could use this tool to gain
431 administrative access to RT. It is incredibly important that nonprivileged
432 users not be allowed to run this tool. It is suggested that you create a
433 non-privileged unix user with the correct group membership and RT access to
434 run this tool.
435
436
437 =head1 OPTIONS
438
439 =over
440
441 =item search 
442
443 Specify the search module you want to use
444
445 =item search-arg 
446
447 An argument to pass to --search
448
449 =item condition
450
451 Specify the condition module you want to use
452
453 =item condition-arg
454
455 An argument to pass to --condition
456
457 =item action 
458
459 Specify the action module you want to use
460
461 =item action-arg
462
463 An argument to pass to --action
464
465 =item template
466
467 Specify name or id of template(s) you want to use
468
469 =item transaction
470
471 Specify if you want to use either 'first', 'last' or 'all' transactions
472
473
474 =item transaction-type
475
476 Specify the comma separated list of transactions' types you want to use
477
478 =item log
479
480 Adjust LogToScreen config option
481
482 =item verbose
483
484 Output status updates to STDOUT
485
486 =back
487