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