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