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