#!/usr/bin/perl # IBM_PROLOG_BEGIN_TAG # This is an automatically generated prolog. # # bos720 src/bos/usr/sbin/install/suma/lib/SUMA/Messenger.pm 1.10 # # Licensed Materials - Property of IBM # # Restricted Materials of IBM # # COPYRIGHT International Business Machines Corp. 2004,2008 # All Rights Reserved # # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with IBM Corp. # # IBM_PROLOG_END_TAG package SUMA::Messenger; # code starts after '=cut' =head1 NAME SUMA::Messenger - Class for displaying messages and reporting status or errors via e-mail or logfile. =head1 SYNOPSIS $rc = SUMA::Messenger->start($policy_obj); $rc = mesg(LVL_MACRO, MSG_MACRO, arg1 ... argN); $rc = SUMA::Messenger->complete; $rc = SUMA::Messenger->log_download(filename); =head1 DESCRIPTION Provides an OO interface for reporting errors, warnings, and information related to the Service Update Management Assistant, as well as making the information available in a log file, on the screen, and/or via e-mail notification. All information is reported in a style defined by maximum threshold values, as specified by either the command line or global configuration specifications. =cut use strict; use lib qw(/usr/suma/lib); use POSIX qw(strftime); sub ckPrivate { my @cklist = @_; my $callee = caller(0); push @cklist, $callee; my $caller = caller(1); for(@cklist) { return 1 if(($caller eq $_) || $caller->isa($_)); } return 0; } # ckPrivate use constant MEGABYTE => 1024 * 1024; use SUMA::GConfig; use locale; our(@ISA, @EXPORT_OK); my($msgmap, $msgmapfh, $macro, $screenlevel, $loglevel, $notifylevel, $logfile, $logfh, $dlogfile, $dlogfh, $maxsize, $email, $ua, $STARTED, $notifier_pid, $notify_writer); $STARTED = 0; # This is set true when "start" is called my $fh_by_pid = {}; my $called_from = $ENV{'_SUMA'} || ''; sub _getMsgFH() { return $fh_by_pid->{$$} if (exists($fh_by_pid->{$$}) && defined($fh_by_pid->{$$})); my $fh; open($fh, $msgmap) or die "Fatal error in messenger: couldn't open map file '$msgmap': $!"; $fh_by_pid->{$$} = $fh; return $fh; } $msgmapfh = _getMsgFH(); local $/ = "\n,"; my($pos, $evalstr); while(<$msgmapfh>) { if(($macro) = /^(MSG_\w+)\s/) { $pos = (tell($msgmapfh) - length) or die("Fatal error importing message $macro - tell() failed: $!"); $pos =~ /(\d+)/; $pos = $1; # *$macro = eval(qq|sub() {"$pos"}|); # The old way - required 'no strict' ($evalstr) = "use constant $macro => $pos" =~ /(.*)/; eval($evalstr); push @EXPORT_OK, $macro; } } # Always export 'mesg'. sub import { SUMA::Messenger->export_to_level(1, @_, 'mesg'); } BEGIN { require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw( mesg start complete log_download ); $msgmap = getGlobal('MSG_MAPFILE_PATH'); die "Message map file '$msgmap' doesn't exist." unless -e $msgmap; # use subs ("_getMsgFH"); $msgmapfh = _getMsgFH(); } =head1 METHODS =over 4 =item mesg LVL_MACRO, MSG_MACRO, [ ARGS ] Accepts a verbosity level, a message macro (representing the message to display), and any arguments (variables) related to the message to display, and returns true or false. If the value of LVL_MACRO is above the current verbosity settings, the message is ignored. Behavior of the ARGS values is comparable to that of the dspmsg command. The MSG_MACRO values are defined by the message map file (/usr/suma/lib/msg.map). Each entry in the file contains all information necessary for the messenger function to produce output, and properly display any translated messages. Acceptable values for LVL_MACRO are as follows: =over 4 =item LVL_ERROR Highly important messages and critical failures. These messages are always reported. A system using this level will have the smallest logfiles/least verbose notices. May also use "undef" when calling the mesg function. =item LVL_WARNING Non-critical failures/error messages/warnings. These messages would be less severe than those at level 0, but may still be an indicator of a problem to investigate. =item LVL_INFO Informational messages. One could reasonably expect an amount of data comparable to that of smitty. A system with a NOTIFY_VERBOSE (a.k.a. e-mail notices) above LVL_INFO is not recommended, as it could generate a lot of mail traffic! =item LVL_VERBOSE Informational messages. These include things such as "contacting server" and "server contacted" (handshaking). A system using this level should expect to log a lot of data - and plan maximum log sizes accordingly. =item LVL_DEBUG Debug verbosity. This should be used if a message is useful only for debug purposes. A system using this level should expect a lot of data to be recorded in the logfile. Increasing the maximum logfile size is strongly encouraged. Such messages are not necessarily translated, and may be identified by the string "DEBUG". =back All of the level macros are exported, and may be used by all parts of the Service Update Management Assistant. =cut ## Method lookup ################################################ sub lookup($@) { my $pos = shift; seek($msgmapfh, $pos, 0) or die "Fatal error in messenger: couldn't seek in map file: $!"; my $line; { local $/ = "\n,"; $line = <$msgmapfh>; chomp $line; } my($cat, $set, $msg, $def) = $line =~ /^\w+\s+(\S+)\s+(\d+)\s+(\d+)\s+(.*)/s; # $ENV{LANG} =~ m/(\w+)/; # $cat = "/usr/lib/nls/msg/$1/$cat"; require LibExt::CatGetS; LibExt::CatGetS->import('catgets'); my $fmt = catgets($cat, $set, $msg, $def); chomp $fmt; $fmt =~ s/%(\d+\$)?(.)/%$2/g; $fmt .= "\n"; my $str = sprintf($fmt, @_); chomp $str; return $str; } # lookup ## Method mesg ################################################## sub mesg($$@) { my($lvl, $pos) = (shift, shift); $msgmapfh = _getMsgFH(); # Validate $lvl within meaningful ranges: $lvl = LVL_ERROR unless defined($lvl); $lvl = -1 if ($lvl < -1); $lvl = LVL_DEBUG if ($lvl >= LVL_DEBUG); local $, = "\n"; local $\ = "\n"; my $str; my $strprefix = $lvl == LVL_DEBUG ? 'DEBUG: ' : ''; my $type; SWITCH: { $type = "E", last SWITCH if ($lvl == LVL_ERROR); $type = "W", last SWITCH if ($lvl == LVL_WARNING); $type = "I", last SWITCH if ($lvl == LVL_INFO); $type = "V", last SWITCH if ($lvl == LVL_VERBOSE); $type = "D", last SWITCH if ($lvl == LVL_DEBUG); } my($callP,$callF,$callL) = caller; unless($called_from eq 'cron') { # Only print to screen if not scheduled if($called_from eq 'SMIT') { # Ensure F4 lists (etc.) get the right output $screenlevel = LVL_WARNING; } else { $screenlevel = getGlobal('SCREEN_VERBOSE'); unless(defined($screenlevel)) { $screenlevel = LVL_WARNING } } ## Print to the screen (STDOUT or STDERR) ################ if (($screenlevel >= $lvl) && ($pos != MSG_LOG_STOP()) && ($pos != MSG_LOG_START())) { start(); my $fh = $lvl <= LVL_WARNING ? \*STDERR : \*STDOUT; $str = lookup($pos, @_) unless $str; if (($lvl == LVL_DEBUG) || ($screenlevel == LVL_DEBUG)) { $str .= " ($callP, $callF \[$callL\])"; } print($fh "$strprefix$str"); } } $loglevel = getGlobal('LOGFILE_VERBOSE'); unless(defined($loglevel)) { $loglevel = LVL_INFO } ## Write to the log file ################################# if($loglevel >= $lvl) { start(); unless($logfh) { open $logfh, ">>$logfile" or die "Unable to open log: $logfile"; } $str = lookup($pos, @_) unless $str; if (($lvl == LVL_DEBUG) || ($loglevel == LVL_DEBUG)) { $str .= " ($callP, $callF \[$callL\])"; } my $time = strftime("%Y" . "%m" . "%d %H:%M:%S",localtime); print($logfh "$time $type $strprefix$str"); } if($notify_writer) { # Only email if scheduled AND the pipe is open $notifylevel = getGlobal('NOTIFY_VERBOSE'); unless(defined($notifylevel)) { $notifylevel = LVL_INFO } ## Add to the notify/e-mail body ######################## if (($notifylevel >= $lvl) && (defined($email))) { start(); $str = lookup($pos, @_) unless $str; if (($lvl == LVL_DEBUG) || ($notifylevel == LVL_DEBUG)) { $str .= " ($callP, $callF \[$callL\])"; } print $notify_writer "$strprefix$str"; } } return 1; } # mesg ## Method start ################################################# =item start POLICY_OBJ Signals the messenger to begin logging and, if appropriate, saving messages for emailing. Causes a 'start' stamp to be written to the log file. A policy object is required, so that certain data may be used to display messages according to system settings. =cut sub start { # This chunk is before the if($STARTED) because we might have # implicit start()s before the first explicit start($policy). This # way we always catch it the first time we get a $policy. if(($called_from eq 'cron') && !$notify_writer) { # In case we were called implicitly, without a Policy, always # check for a policy if $notify_writer isn't set up my $pol = shift; # Fork Notify Writer: FNW: if(defined($pol) && ref($pol) && $pol->isa('SUMA::Policy')) { $email = $pol->getField("NotifyEmail"); if ($email eq '') { undef $email; last FNW; } $notifier_pid = open($notify_writer, '|-'); # Safe pipe open die "Unable to fork notifier subprocess: $!" unless defined $notifier_pid; # Parent continues happily on unless($notifier_pid) { # Notifier child process $STARTED = 0; # So we don't complete() on END require LWP::UserAgent; require HTTP::Request::Common; HTTP::Request::Common->import('POST'); my $line; my $content=0; my $req = POST("mailto:$email", Subject => lookup(MSG_TASK_RESULTS()), content_type => "text/plain"); while($line=) { # Read from the safe pipe if ($line ne ''){ $content++; } $req->add_content($line); } # We get here when the pipe closes; send the email if the body of the mail has data. if ($content != 0) { $ua = LWP::UserAgent->new(timeout => 30); my $resp = $ua->request($req); exit $resp->is_success(); } } # Notifier child process } } # If logging is already started, don't do it again if ($STARTED) { return $STARTED; } $STARTED = 1; # Signal that logging has started ###################################################### # Load up the Global Config's log-related settings # # and store default values, if getGlobal fails on us # ###################################################### unless($logfile) { $logfile = getGlobal('LOGFILE') || "/var/adm/ras/suma.log"; } unless($dlogfile) { $dlogfile = getGlobal('DL_HIST_LOGFILE') || "/var/adm/ras/suma_dl.log"; } unless(defined($maxsize)) { $maxsize = getGlobal('MAXLOGSIZE_MB') || 1; # Convert to bytes $maxsize *= MEGABYTE; } ## Check both logs for filesize. If they've gone over our limit ## we need to roll them off to a backup copy and create new ones. if (-e $logfile && ((-s $logfile) > $maxsize)) { rename $logfile,$logfile.".bak" } if (-e $dlogfile && ((-s $dlogfile) > $maxsize)) { rename $dlogfile,$dlogfile.".bak" } my $time = strftime("%a %b %e %H:%M:%S %Y",localtime); mesg(LVL_INFO, MSG_LOG_START(), $time); return $STARTED; } # start ## Method complete ############################################## =item complete Signals the end of current logging operations. When executed, this records an 'end' stamp to be written in the log file, collects all messages stored for e-mail notification, and sends the e-mail. It returns either true or false. =cut sub complete { my $time = strftime("%a %b %e %H:%M:%S %Y",localtime); mesg(LVL_INFO, MSG_LOG_STOP(), $time); mesg(LVL_DEBUG, MSG_GENERIC(), "Closing file handles"); _destruct(); } # complete sub _destruct { close($logfh) if $logfh; close($dlogfh) if $dlogfh; if ($notifier_pid && $notify_writer) { # Let the notifier subproc know we're done by closing the writer close($notify_writer); # Now wait for it to send the email waitpid($notifier_pid, 0); } $STARTED = 0; return 1; } ## Method log_download ########################################## =item log_download FILENAME Records successful file transfers to the download history logfile, and may only be called by SUMA::Download. All other requests for this function result in an error message and failed return code. It returns either true or false. =cut sub log_download { my($fname) = (shift); # Only SUMA::Download may call this function! return 0 unless ckPrivate('SUMA::Download'); unless($dlogfh) { open $dlogfh, ">>$dlogfile" or die "Unable to open download log: $dlogfile"; chmod 0664, $logfile, $dlogfile; } chomp($fname); local $\ = "\n"; my $time = strftime("%Y" . "%m" . "%d %H:%M:%S",localtime); print($dlogfh "$time $fname") || return 0; return 1; } # log_download END { if ($STARTED) { _destruct(); } } 1;