1 package OpenILS::Utils::Cronscript;
3 # ---------------------------------------------------------------
4 # Copyright (C) 2010 Equinox Software, Inc
5 # Author: Joe Atzberger <jatzberger@esilibrary.com>
6 # Portions Copyright (C) 2011 Merrimack Valley Library Consortium
7 # Author: Jason Stephenson <jstephenson@mvlc.org>
9 # This program is free software; you can redistribute it and/or
10 # modify it under the terms of the GNU General Public License
11 # as published by the Free Software Foundation; either version 2
12 # of the License, or (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 # ---------------------------------------------------------------
20 # The purpose of this module is to consolidate the common aspects
21 # of various cron tasks that all need the same things:
22 # ~ non-duplicative processing, i.e. lockfiles and lockfile checking
23 # ~ opensrf_core.xml file location
24 # ~ common options like help and debug
29 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
31 use OpenSRF::AppSession;
32 use OpenSRF::Utils::JSON;
33 use OpenSRF::EX qw(:try);
34 use OpenILS::Utils::Fieldmapper;
35 use OpenILS::Utils::Lockfile;
36 use OpenILS::Utils::CStoreEditor q/:funcs/;
37 use OpenILS::Application::AppUtils;
39 use File::Basename qw/fileparse/;
44 # Added for authentication
45 use Digest::MD5 qw/md5_hex/;
47 our @extra_opts = ( # additional keys are stored here
53 my $apputils = 'OpenILS::Application::AppUtils';
59 # default_opts_clean => {},
61 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
62 'osrf-config=s' => '@sysconfdir@/opensrf_core.xml',
66 # 'internal_var' => 'XYZ',
72 auto_get_options_4_bootstrap => 1,
77 my $key = shift or return 1;
78 $key =~ /[=:].*$/ and return 0;
79 $key =~ /[+!]$/ and return 0;
84 my $key = shift or return;
90 sub fuzzykey { # when you know the hash you want from, but not the exact key
91 my $self = shift or return;
92 my $key = shift or return;
93 my $target = @_ ? shift : 'opts_clean';
94 foreach (map {clean($_)} keys %{$self->{default_opts}}) { # TODO: cache
95 $key eq $_ and return $self->{$target}->{$_};
100 # A wrapper around GetOptions
101 # {opts} does two things for GetOptions (see Getopt::Long)
102 # (1) maps command-line options to the *other* variables where values are stored (in opts_clean)
103 # (2) provides hashspace for the rest of the arbitrary options from the command-line
105 # TODO: allow more options to be passed here, maybe mimic Getopt::Long::GetOptions style
107 # If an arrayref argument is passed, then @ARGV will NOT be touched.
108 # Instead, the array will be passed to GetOptionsFromArray.
113 my $arrayref = @_ ? shift : undef;
114 if ($arrayref and ref($arrayref) ne 'ARRAY') {
115 carp "MyGetOptions argument is not an array ref. Expect GetOptionsFromArray to explode";
117 $self->{got_options} and carp "MyGetOptions called after options were already retrieved previously";
118 my @keys = sort {is_clean($b) <=> is_clean($a)} keys %{$self->{default_opts}};
119 $debug and print "KEYS: ", join(", ", @keys), "\n";
121 my $clean = clean($_);
122 my $place = $self->{default_opts_clean}->{$clean};
123 $self->{opts_clean}->{$clean} = $place; # prepopulate default
124 # $self->{opts}->{$_} = $self->{opts_clean}->{$clean}; # pointer for GetOptions
125 $self->{opts}->{$_} = sub {
128 ref ( $self->{opts_clean}->{$opt} ) and ref($self->{opts_clean}->{$opt}) eq 'SCALAR'
129 and ${$self->{opts_clean}->{$opt}} = $val; # set the referent's value
130 $self->{opts_clean}->{$opt} = $val; # burn the map, stick the value there
131 }; # pointer for GetOptions
133 $arrayref ? GetOptionsFromArray($arrayref, $self->{opts}, @keys)
134 : GetOptions( $self->{opts}, @keys) ;
137 delete $self->{opts}->{$_}; # now remove the mappings from (1) so we just have (2)
139 $self->clean_mirror('opts'); # populate clean_opts w/ cleaned versions of (2), plus everything else
141 print $self->help() and exit if $self->{opts_clean}->{help};
142 $self->new_lockfile();
143 $self->{got_options}++;
144 return wantarray ? %{$self->{opts_clean}} : $self->{opts_clean};
149 $debug and $OpenILS::Utils::Lockfile::debug = $debug;
150 unless ($self->{opts_clean}->{nolockfile} || $self->{default_opts_clean}->{nolockfile}) {
151 $self->{lockfile_obj} = OpenILS::Utils::Lockfile->new($self->first_defined('lock-file'));
152 $self->{lockfile} = $self->{lockfile_obj}->filename;
158 my $key = shift or return;
159 foreach (qw(opts_clean opts default_opts_clean default_opts)) {
160 defined $self->{$_}->{$key} and return $self->{$_}->{$key};
167 my $dirty = @_ ? shift : 'default_opts';
168 foreach (keys %{$self->{$dirty}}) {
169 defined $self->{$dirty}->{$_} or next;
170 $self->{$dirty . '_clean'}->{clean($_)} = $self->{$dirty}->{$_};
176 my $self = _default_self;
177 bless ($self, $class);
179 $debug and print "new ", __PACKAGE__, " obj: ", Dumper($self);
187 my $clean = clean($key);
188 my @others = grep {/$clean/ and $_ ne $key} keys %{$self->{default_opts}};
190 $debug and print "unique key $key => $val\n";
191 $self->{default_opts}->{$key} = $val; # no purge, just add
195 $debug and print "variant of $key => $_\n";
196 if ($key ne $clean) { # if it is a dirtier key, delete the clean one
197 delete $self->{default_opts}->{$_};
198 $self->{default_opts}->{$key} = $val;
199 } else { # else update the dirty one
200 $self->{default_opts}->{$_} = $val;
205 sub init { # not INIT
207 my $opts = @_ ? shift : {}; # user can specify more default options to constructor
208 # TODO: check $opts is hashref; then check verbose/debug first. maybe check negations e.g. "no-verbose" ?
209 @extra_opts = keys %$opts;
210 foreach (@extra_opts) { # add any other keys w/ default values
211 $debug and print "init() adding option $_, default value: $opts->{$_}\n";
212 $self->add_and_purge($_, $opts->{$_});
220 return "\nUSAGE: $0 [OPTIONS]";
225 my $chunk = @_ ? shift : '';
229 --osrf-config </path/to/config_file> Default: $self->{default_opts_clean}->{'osrf-config'}
230 Specify OpenSRF core config file.
232 --lock-file </path/to/file_name> Default: $self->{default_opts_clean}->{'lock-file'}
237 --debug Print server responses to STDOUT for debugging
238 --verbose Set verbosity
239 --help Show this help message
245 return $self->usage() . "\n" . $self->options_help(@_) . $self->example();
249 return "\n\nEXAMPLES:\n\n $0 --osrf-config /my/other/opensrf_core.xml\n";
252 # the proper order is: MyGetOptions, bootstrap, session.
253 # But the latter subs will check to see if they need to call the preceeding one(s).
256 my $self = shift or return;
257 $self->{bootstrapped} or $self->bootstrap();
258 @_ or croak "session() called without required argument (app_name, e.g. 'open-ils.acq')";
259 return OpenSRF::AppSession->create(@_);
263 my $self = shift or return;
264 if ($self->{auto_get_options_4_bootstrap} and not $self->{got_options}) {
265 $debug and print "Automatically calling MyGetOptions before bootstrap\n";
266 $self->MyGetOptions();
269 $debug and print "bootstrap lock-file : ", $self->first_defined('lock-file'), "\n";
270 $debug and print "bootstrap osrf-config: ", $self->first_defined('osrf-config'), "\n";
271 OpenSRF::System->bootstrap_client(config_file => $self->first_defined('osrf-config'));
272 Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
273 $self->{bootstrapped} = 1;
275 $self->{bootstrapped} = 0;
281 my $self = shift or return;
282 OpenILS::Utils::CStoreEditor::init(); # no return value to check
283 $self->{editor_inited} = 1;
287 my $self = shift or return;
288 $self->{bootstrapped} or $self->bootstrap();
289 $self->{editor_inited} or $self->editor_init();
290 return new_editor(@_);
293 # Die on an event. Takes an optional third parameter for the textcode
294 # of an event to die on. If the event textcode does not match the
295 # third parameter, will warn on the event instead of dying.
300 if ($apputils->event_code($e)) {
301 if (!defined($name) || $apputils->event_equals($e,$name)) {
309 # Prints warning on an even. Takes an optional third parameter for the
310 # textcode of an event to warn on.
315 if ($apputils->event_code($e)
316 && (!defined($name) || $apputils->event_equals($e, $name))) {
321 # Authenticate with the open-ils.auth module.
322 # Takes a hash ref of arguments:
324 # username => username to authenticate as,
325 # password => the user's password,
326 # workstation => the workstation to use (optional),
327 # type => the type of login (optional, but defaults to staff)
330 # returns the authtoken or undef on failure.
331 # Also stores the authtoken and authtime as fields on the object.
333 my $self = shift or return;
334 my $args = shift or return;
335 if ($args && ref($args) eq 'HASH') {
336 # Default to staff in case the back end ever stops doing so.
337 $args->{type} = 'staff' unless (defined($args->{type}));
339 my $session = $self->session('open-ils.auth');
340 my $seed = $session->request(
341 'open-ils.auth.authenticate.init', $args->{'username'}
344 $args->{password} = md5_hex($seed . md5_hex($args->{password}));
345 my $req = $session->request(
346 'open-ils.auth.authenticate.complete', $args
349 my $response = $req->gather(1);
350 if ($response && ref($response) eq 'HASH' && $response->{payload}) {
351 $self->{authtoken} = $response->{payload}->{authtoken};
352 $self->{authtime} = $response->{payload}->{authtime};
354 $self->{authtoken} = undef;
355 $self->{authtime} = undef;
356 carp("Authentication failed");
358 $session->disconnect();
359 return $self->authtoken;
365 # Deletes the session for our authtoken if we have logged in with the
366 # authenticate method.
368 my $self = shift or return;
369 my $token = shift || $self->{authtoken};
371 my $session = $self->session('open-ils.auth');
372 if ($session->request('open-ils.auth.session.delete', $token)->gather(1)) {
373 if ($token eq $self->{authtoken}) {
374 $self->{authtoken} = undef;
375 $self->{authtime} = undef;
378 carp("Not authenticated");
380 $session->disconnect();
382 carp("No authtoken");
388 return $self->{authtoken};
393 return $self->{authtime};
403 OpenILS::Utils::Cronscript - Consolidated options handling and utility
404 methods for any script (not just cron, really)
408 use OpenILS::Utils::Cronscript;
411 'min=i' => 0, # keys are Getopt::Long style options
412 'max=i' => 999, # values are default values
418 my $core = OpenILS::Utils::Cronscript->new(\%defaults);
419 my $opts = $core->MyGetOptions(); # options now in, e.g.: $opts->{max}
422 You can skip alot of the above if you're happy with the defaults:
424 my $script = OpenILS::Utils::Cronscript->new();
426 If you just don't want a lock file:
428 my $core = OpenILS::Utils::Cronscript->new({nolockfile=>1});
430 Or if you don't need any additional options and just want to get a
433 use OpenILS::Utils::Cronscript;
434 my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
436 Cronscript gives you access to many useful methods:
438 You can login if necessary:
442 password => 'password',
443 workstation => 'workstation_name', # optional
444 type => 'staff' # optional, but staff is the default
446 my $authtoken = $core->authenticate($account);
448 You can logout a session given its authtoken:
450 $core->logout($authtoken);
452 Or, if you've authenticated with the authenticate method, you can
453 logout the most recently authenticated session:
457 If you have logged in with the authenticate method, you can retrieve
458 your current authtoken or authtime values:
460 my $token = $core->authtoken;
461 my $authtime = $core->authtime;
463 You can create a CStoreEdtor object:
465 my $editor = $core->editor(); # With defaults.
466 my $editor = $core->editor(authtoken=>$authtoken); # with a given
468 my $editor = $core->editor(xact=>1); # With transactions or any
469 # other CStoreEditor options.
471 You can create OpenSRF AppSesions to run commands:
473 my $pcrud = $core->session('open-ils.pcrud');
474 #...Do some pcrud stuff here.
476 You can print warnings or die on events:
479 $core->warn_event($evt);
480 $core->die_event($evt);
482 Or only on certain events:
484 $core->warn_event($evt, 'PERM_FAILURE');
485 $core->die_event($evt, 'PERM_FAILURE');
487 Includes a shared debug flag so you can turn debug mode on and off:
489 $OpenILS::Utils::Cronscript::debug = 1; # Debugging on
490 $OpenILS::Utils::Cronscript::debug = 0; # Debugging off
492 Includes OpenILS::Application::Apputils so using AppUtils methods is
495 my $apputils = 'OpenILS::Application::AppUtils';
496 $apputils->event_code($evt);
498 Uses and imports the OpenILS::Utils::Fieldmapper so you don't have to.
502 There are a few main problems when writing a new script for Evergreen.
504 =head2 Initialization
506 The runtime environment for the application requires a lot of
507 initialization, but during normal operation it has already occured
508 (when Evergreen was started). So most of the EG code never has to
509 deal with this problem, but standalone scripts do. The timing and
510 sequence of requisite events is important and not obvious.
512 =head2 Common Options, Consistent Options
514 We need several common options for each script that accesses the
515 database or uses EG data objects and methods. Logically, these
516 options often deal with initialization. They should take the B<exact>
517 same form(s) for each script and should not be dependent on the local
518 author to copy and paste them from some reference source. We really
519 don't want to encourage (let alone force) admins to use C<--config>,
520 C<--osrf-confg>, C<-c>, and C<@ARGV[2]> for the same purpose in
521 different scripts, with different default handling, help descriptions
522 and error messages (or lack thereof).
524 This suggests broader problem of UI consistency and uniformity, also
525 partially addressed by this module.
529 A lockfile is necessary for a script that wants to prevent possible
530 simultaneous execution. For example, consider a script that is
531 scheduled to run frequently, but that experiences occasional high
532 load: you wouldn't want crontab to start running it again if the first
533 instance had not yet finished.
535 But the code for creating, writing to, checking for, reading and
536 cleaning up a lockfile for the script bloats what might otherwise be a
537 terse method call. Conscript handles lockfile generation and removal
542 The common options (and default values) are:
544 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
545 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
553 OpenILS::Application::AppUtils
554 OpenILS::Utils::Fieldmapper
555 OpenILS::Utils::Lockfile
559 Joe Atzberger <jatzberger@esilibrary.com>
560 Jason Stephenson <jstephenson@mvlc.org>