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};
398 my $cache = "OpenSRF::Utils::Cache";
400 $self->{memcache} = $cache->new('global') unless $self->{memcache};
401 return $self->{memcache};
411 OpenILS::Utils::Cronscript - Consolidated options handling and utility
412 methods for any script (not just cron, really)
416 use OpenILS::Utils::Cronscript;
419 'min=i' => 0, # keys are Getopt::Long style options
420 'max=i' => 999, # values are default values
426 my $core = OpenILS::Utils::Cronscript->new(\%defaults);
427 my $opts = $core->MyGetOptions(); # options now in, e.g.: $opts->{max}
430 You can skip alot of the above if you're happy with the defaults:
432 my $script = OpenILS::Utils::Cronscript->new();
434 If you just don't want a lock file:
436 my $core = OpenILS::Utils::Cronscript->new({nolockfile=>1});
438 Or if you don't need any additional options and just want to get a
441 use OpenILS::Utils::Cronscript;
442 my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
444 Cronscript gives you access to many useful methods:
446 You can login if necessary:
450 password => 'password',
451 workstation => 'workstation_name', # optional
452 type => 'staff' # optional, but staff is the default
454 my $authtoken = $core->authenticate($account);
456 You can logout a session given its authtoken:
458 $core->logout($authtoken);
460 Or, if you've authenticated with the authenticate method, you can
461 logout the most recently authenticated session:
465 If you have logged in with the authenticate method, you can retrieve
466 your current authtoken or authtime values:
468 my $token = $core->authtoken;
469 my $authtime = $core->authtime;
471 You can create a CStoreEdtor object:
473 my $editor = $core->editor(); # With defaults.
474 my $editor = $core->editor(authtoken=>$authtoken); # with a given
476 my $editor = $core->editor(xact=>1); # With transactions or any
477 # other CStoreEditor options.
479 You can create OpenSRF AppSesions to run commands:
481 my $pcrud = $core->session('open-ils.pcrud');
482 #...Do some pcrud stuff here.
484 You can print warnings or die on events:
487 $core->warn_event($evt);
488 $core->die_event($evt);
490 Or only on certain events:
492 $core->warn_event($evt, 'PERM_FAILURE');
493 $core->die_event($evt, 'PERM_FAILURE');
495 Includes a shared debug flag so you can turn debug mode on and off:
497 $OpenILS::Utils::Cronscript::debug = 1; # Debugging on
498 $OpenILS::Utils::Cronscript::debug = 0; # Debugging off
500 Includes OpenILS::Application::Apputils so using AppUtils methods is
503 my $apputils = 'OpenILS::Application::AppUtils';
504 $apputils->event_code($evt);
506 Uses and imports the OpenILS::Utils::Fieldmapper so you don't have to.
510 There are a few main problems when writing a new script for Evergreen.
512 =head2 Initialization
514 The runtime environment for the application requires a lot of
515 initialization, but during normal operation it has already occured
516 (when Evergreen was started). So most of the EG code never has to
517 deal with this problem, but standalone scripts do. The timing and
518 sequence of requisite events is important and not obvious.
520 =head2 Common Options, Consistent Options
522 We need several common options for each script that accesses the
523 database or uses EG data objects and methods. Logically, these
524 options often deal with initialization. They should take the B<exact>
525 same form(s) for each script and should not be dependent on the local
526 author to copy and paste them from some reference source. We really
527 don't want to encourage (let alone force) admins to use C<--config>,
528 C<--osrf-confg>, C<-c>, and C<@ARGV[2]> for the same purpose in
529 different scripts, with different default handling, help descriptions
530 and error messages (or lack thereof).
532 This suggests broader problem of UI consistency and uniformity, also
533 partially addressed by this module.
537 A lockfile is necessary for a script that wants to prevent possible
538 simultaneous execution. For example, consider a script that is
539 scheduled to run frequently, but that experiences occasional high
540 load: you wouldn't want crontab to start running it again if the first
541 instance had not yet finished.
543 But the code for creating, writing to, checking for, reading and
544 cleaning up a lockfile for the script bloats what might otherwise be a
545 terse method call. Conscript handles lockfile generation and removal
550 The common options (and default values) are:
552 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
553 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
561 OpenILS::Application::AppUtils
562 OpenILS::Utils::Fieldmapper
563 OpenILS::Utils::Lockfile
567 Joe Atzberger <jatzberger@esilibrary.com>
568 Jason Stephenson <jstephenson@mvlc.org>