0642a768d8b3b50d2218a223411d2735020e5cae
[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-2013 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 warnings;
51 use Carp;
52
53 # fix lib paths, some may be relative
54 BEGIN {
55     require File::Spec;
56     my @libs = ("lib", "local/lib");
57     my $bin_path;
58
59     for my $lib (@libs) {
60         unless ( File::Spec->file_name_is_absolute($lib) ) {
61             unless ($bin_path) {
62                 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
63                     $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
64                 }
65                 else {
66                     require FindBin;
67                     no warnings "once";
68                     $bin_path = $FindBin::Bin;
69                 }
70             }
71             $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
72         }
73         unshift @INC, $lib;
74     }
75
76 }
77
78 use RT;
79
80 use Getopt::Long;
81
82 use RT::Interface::CLI qw(CleanEnv GetCurrentUser GetMessageContent loc);
83
84 #Clean out all the nasties from the environment
85 CleanEnv();
86
87 my ( $search, $condition, $action, $search_arg, $condition_arg, $action_arg,
88      $template, $template_id, $transaction, $transaction_type, $help, $log, $verbose );
89 GetOptions(
90     "search=s"           => \$search,
91     "search-arg=s"       => \$search_arg,
92     "condition=s"        => \$condition,
93     "condition-arg=s"    => \$condition_arg,
94     "action-arg=s"       => \$action_arg,
95     "action=s"           => \$action,
96     "template=s"         => \$template,
97     "template-id=s"      => \$template_id,
98     "transaction=s"      => \$transaction,
99     "transaction-type=s" => \$transaction_type,
100     "log=s"              => \$log,
101     "verbose|v"          => \$verbose,
102     "help"               => \$help,
103 );
104
105 # Load the config file
106 RT::LoadConfig();
107
108 # adjust logging to the screen according to options
109 RT->Config->Set( LogToScreen => $log ) if $log;
110
111 #Connect to the database and get RT::SystemUser and RT::Nobody loaded
112 RT::Init();
113
114 require RT::Tickets;
115 require RT::Template;
116
117 #Get the current user all loaded
118 my $CurrentUser = GetCurrentUser();
119
120 # show help even if there is no current user
121 help() if $help;
122
123 unless ( $CurrentUser->Id ) {
124     print loc("No RT user found. Please consult your RT administrator.");
125     exit(1);
126 }
127
128 help() unless $search && $action;
129
130 $transaction = lc( $transaction||'' );
131 if ( $transaction && $transaction !~ /^(first|all|last)$/i ) {
132     print STDERR loc("--transaction argument could be only 'first', 'last' or 'all'");
133     exit 1;
134 }
135
136 if ( $template && $template_id ) {
137     print STDERR loc("--template-id is deprecated argument and can not be used with --template");
138     exit 1;
139 }
140 elsif ( $template_id ) {
141 # don't warn
142     $template = $template_id;
143 }
144
145 # We _must_ have a search object
146 load_module($search);
147 load_module($action)    if ($action);
148 load_module($condition) if ($condition);
149
150 my $void_scrip = RT::Scrip->new( $CurrentUser );
151 my $void_scrip_action = RT::ScripAction->new( $CurrentUser );
152
153 #At the appointed time:
154
155 #find a bunch of tickets
156 my $tickets = RT::Tickets->new($CurrentUser);
157 $search  = $search->new(
158     TicketsObj  => $tickets,
159     Argument    => $search_arg,
160     CurrentUser => $CurrentUser
161 );
162 $search->Prepare();
163
164 #for each ticket we've found
165 while ( my $ticket = $tickets->Next() ) {
166     print $ticket->Id() . ":\n" if ($verbose);
167
168     my $template_obj = get_template( $ticket );
169
170     if ( $transaction ) {
171         my $txns = get_transactions($ticket);
172         my $found = 0;
173         while ( my $txn = $txns->Next ) {
174             print "\t".loc("Using transaction #[_1]...", $txn->id)."\n"
175                 if $verbose;
176             process($ticket, $txn, $template_obj);
177             $found = 1;
178         }
179         print "\t".loc("Couldn't find suitable transaction, skipping")."\n"
180             if $verbose && !$found;
181     } else {
182         print "\t".loc("Processing without transaction, some conditions and actions may fail. Consider using --transaction argument")."\n"
183             if $verbose;
184
185         process($ticket, undef, $template_obj);
186     }
187 }
188
189 sub process {
190     my $ticket = shift;
191     my $transaction = shift;
192     my $template_obj = shift;
193
194     # perform some more advanced check
195     if ($condition) {
196         my $condition_obj = $condition->new(
197             TransactionObj => $transaction,
198             TicketObj      => $ticket,
199             ScripObj       => $void_scrip,
200             TemplateObj    => $template_obj,
201             Argument       => $condition_arg,
202             CurrentUser    => $CurrentUser,
203         );
204
205         # if the condition doesn't apply, get out of here
206
207         return unless $condition_obj->IsApplicable;
208         print "\t".loc("Condition matches...")."\n" if $verbose;
209     }
210
211     #prepare our action
212     my $action_obj = $action->new(
213         TicketObj      => $ticket,
214         TransactionObj => $transaction,
215         TemplateObj    => $template_obj,
216         Argument       => $action_arg,
217         ScripObj       => $void_scrip,
218         ScripActionObj => $void_scrip_action,
219         CurrentUser    => $CurrentUser,
220     );
221
222     #if our preparation, move onto the next ticket
223     return unless $action_obj->Prepare;
224     print "\t".loc("Action prepared...")."\n" if $verbose;
225
226     #commit our action.
227     return unless $action_obj->Commit;
228     print "\t".loc("Action committed.")."\n" if $verbose;
229 }
230
231 # =head2 get_transactions
232
233 # Takes ticket and returns L<RT::Transactions> object with transactions
234 # of the ticket according to command line arguments C<--transaction>
235 # and <--transaction-type>.
236
237 # =cut
238
239 sub get_transactions {
240     my $ticket = shift;
241     my $txns = $ticket->Transactions;
242     my $order = $transaction eq 'last'? 'DESC': 'ASC';
243     $txns->OrderByCols(
244         { FIELD => 'Created', ORDER => $order },
245         { FIELD => 'id', ORDER => $order },
246     );
247     if ( $transaction_type ) {
248         $transaction_type =~ s/^\s+//;
249         $transaction_type =~ s/\s+$//;
250         foreach my $type ( split /\s*,\s*/, $transaction_type ) {
251             $txns->Limit( FIELD => 'Type', VALUE => $type, ENTRYAGGREGATOR => 'OR' );
252         }
253     }
254     $txns->RowsPerPage(1) unless $transaction eq 'all';
255     return $txns;
256 }
257
258 # =head2 get_template
259
260 # Takes a ticket and returns a template according to command line options.
261
262 # =cut
263
264 { my $cache = undef;
265 sub get_template {
266     my $ticket = shift;
267     return undef unless $template;
268
269     unless ( $template =~ /\D/ ) {
270         # by id
271         return $cache if $cache;
272
273         my $cache = RT::Template->new( RT->SystemUser );
274         $cache->Load( $template );
275         die "Failed to load template '$template'"
276             unless $cache->id;
277         return $cache;
278     }
279
280     my $queue = $ticket->Queue;
281     return $cache->{ $queue } if $cache->{ $queue };
282
283     my $res = RT::Template->new( RT->SystemUser );
284     $res->LoadQueueTemplate( Queue => $queue, Name => $template );
285     unless ( $res->id ) {
286         $res->LoadGlobalTemplate( $template );
287         die "Failed to load template '$template', either for queue #$queue or global"
288             unless $res->id;
289     }
290     return $cache->{ $queue } = $res;
291 } }
292
293
294 # =head2 load_module
295
296 # Loads a perl module, dying nicely if it can't find it.
297
298 # =cut
299
300 sub load_module {
301     my $modname = shift;
302     eval "require $modname";
303     if ($@) {
304         die loc( "Failed to load module [_1]. ([_2])", $modname, $@ );
305     }
306
307 }
308
309
310 sub help {
311
312     print loc( "[_1] is a tool to act on tickets from an external scheduling tool, such as cron.", $0 )
313       . "\n";
314     print loc("It takes several arguments:") . "\n\n";
315
316     print "     "
317       . loc( "[_1] - Specify the search module you want to use", "--search" )
318       . "\n";
319     print "     "
320       . loc( "[_1] - An argument to pass to [_2]", "--search-arg", "--search" )
321       . "\n";
322
323     print "     "
324       . loc( "[_1] - Specify the condition module you want to use", "--condition" )
325       . "\n";
326     print "     "
327       . loc( "[_1] - An argument to pass to [_2]", "--condition-arg", "--condition" )
328       . "\n";
329     print "     "
330       . loc( "[_1] - Specify the action module you want to use", "--action" )
331       . "\n";
332     print "     "
333       . loc( "[_1] - An argument to pass to [_2]", "--action-arg", "--action" )
334       . "\n";
335     print "     "
336       . loc( "[_1] - Specify name or id of template(s) you want to use", "--template" )
337       . "\n";
338     print "     "
339       . loc( "[_1] - Specify if you want to use either 'first', 'last' or 'all' transactions", "--transaction" )
340       . "\n";
341     print "     "
342       . loc( "[_1] - Specify the comma separated list of transactions' types you want to use", "--transaction-type" )
343       . "\n";
344     print "     "
345       . loc( "[_1] - Adjust LogToScreen config option", "--log" ) . "\n";
346     print "     "
347       . loc( "[_1] - Output status updates to STDOUT", "--verbose" ) . "\n";
348     print "\n";
349     print "\n";
350     print loc("Security:")."\n";
351     print loc("This tool allows the user to run arbitrary perl modules from within RT.")." ". 
352         loc("If this tool were setgid, a hostile local user could use this tool to gain administrative access to RT.")." ".
353         loc("It is incredibly important that nonprivileged users not be allowed to run this tool."). " " . 
354         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";
355     print "\n";
356     print loc("Example:");
357     print "\n";
358     print " "
359       . loc( "The following command will find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:"
360       )
361       . "\n\n";
362
363     print " bin/rt-crontool \\\n";
364     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
365     print "  --condition RT::Condition::Overdue \\\n";
366     print "  --action RT::Action::SetPriority --action-arg 99 \\\n";
367     print "  --verbose\n";
368
369     print "\n";
370     print loc("Escalate tickets"). "\n";
371     print " bin/rt-crontool \\\n";
372     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
373     print"  --action RT::Action::EscalatePriority\n";
374  
375  
376  
377
378
379
380     exit(0);
381 }
382
383 __END__
384
385 =head1 NAME
386
387 rt-crontool - a tool to act on tickets from an external scheduling tool
388
389 =head1 SYNOPSIS
390
391     # find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:
392     rt-crontool \
393       --search RT::Search::ActiveTicketsInQueue  --search-arg general \
394       --condition RT::Condition::Overdue \
395       --action RT::Action::SetPriority --action-arg 99 \
396       --verbose
397
398     # Escalate tickets
399       rt-crontool \
400         --search RT::Search::ActiveTicketsInQueue  --search-arg general \
401         --action RT::Action::EscalatePriority
402
403 =head1 DESCRIPTION
404
405 This script is a tool to act on tickets from an external scheduling tool, such
406 as cron.
407
408 Security:
409
410 This tool allows the user to run arbitrary perl modules from within RT. If
411 this tool were setgid, a hostile local user could use this tool to gain
412 administrative access to RT. It is incredibly important that nonprivileged
413 users not be allowed to run this tool. It is suggested that you create a
414 non-privileged unix user with the correct group membership and RT access to
415 run this tool.
416
417
418 =head1 OPTIONS
419
420 =over
421
422 =item search 
423
424 Specify the search module you want to use
425
426 =item search-arg 
427
428 An argument to pass to --search
429
430 =item condition
431
432 Specify the condition module you want to use
433
434 =item condition-arg
435
436 An argument to pass to --condition
437
438 =item action 
439
440 Specify the action module you want to use
441
442 =item action-arg
443
444 An argument to pass to --action
445
446 =item template
447
448 Specify name or id of template(s) you want to use
449
450 =item transaction
451
452 Specify if you want to use either 'first', 'last' or 'all' transactions
453
454
455 =item transaction-type
456
457 Specify the comma separated list of transactions' types you want to use
458
459 =item log
460
461 Adjust LogToScreen config option
462
463 =item verbose
464
465 Output status updates to STDOUT
466
467 =back
468