====================================================
mping.pl v1.0
The program uses a tab delimeted text file 'mping.lookup' for input.
+It is made up of the following fields:
"#Ping IP Address" "WAN IP Address" "Loopback IP" "Customer N
+ame" "PVC Name". The only important thing is that they are TAB se
+perated, not just white space.
====================================================
#!/usr/bin/perl -w
use strict;
use IO::Handle;
use Net::Ping;
use Net::SNMP;
use Data::Dumper;
my $source_file = (shift || "./mping.lookup");
my %pid_to_host;
my %host_result;
my @hosts;
my $OID = '.1.3.6.1.4.1.15102.2.1.1.1';
my %address2specific;
open(IPS,"$source_file")
or die "Cannot open source file $source_file: $!\n";
autoflush STDOUT 1;
for (<IPS>)
{
next if $_ =~ m/^\#/;
print "Empty Ping IP Field, skipping: $_\n" unless ($_ =~ m/^\d+\.
+/);
next unless ($_ =~ m/^\d+\./);
s/\cM//;
m/^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$/;
my $ping_ip = $1;
my $wan_ip = $2;
my $loopback_ip = $3;
my $customer_name = $4;
my $lport_string = $5;
if ($ping_ip =~ m/^\d+\.\d+\.\d+\.\d+$/)
{
wait_for_a_kid() if keys %pid_to_host > 30;
if ( my $pid = fork ) {
# parent
$pid_to_host{$pid} = $ping_ip.":".$wan_ip.":".
$loopback_ip.":".$customer_name.
":".$lport_string;
} else {
# child
exit ping_a_host($ping_ip);
}
}
}
1 while wait_for_a_kid();
sub ping_a_host
{
my $host = shift;
my $p = Net::Ping->new('icmp',7);
$p->ping($host) ? 1 : 0;
}
sub wait_for_a_kid
{
my $pid = wait;
my $kid_return = $?;
return 0 if $pid < 0;
my $host = delete $pid_to_host{$pid}
or warn("Why did I see $pid ($kid_return)\n"), next;
my ($ping_ip,
$wan_ip,
$loopback_ip,
$customer_name,
$lport_string) = split(":",$host);
#send_trap('10.51.9.64',
send_trap('ncool1',
'public',
'162',
$ping_ip,
$wan_ip,
$loopback_ip,
$customer_name,
$lport_string,
$OID) if (!($kid_return));
#print "send_trap('10.51.9.64', 'public', '161', $ping_ip, $wan_ip,
+ $loopback_ip, $customer_name, $lport_string, $OID) ",scalar localtim
+e(),"\n" if (!($kid_return));
print "send_trap('ncool1', 'public', '162', $ping_ip, $wan_ip, $loo
+pback_ip, $customer_name, $lport_string, $OID) ", scalar localtime(),
+"\n" if (!($kid_return));
1;
}
sub send_trap
{
my ($host,
$community,
$port,
$ping_ip,
$wan_ip,
$loopback_ip,
$customer_name,
$lport_string,
$OID) = (@_);
my ($session, $error) = Net::SNMP->session(
-hostname => shift || $host,
-community => shift || $community,
-port => shift || $port,
);
print "\nError in creating SNMP session\n" if ($error);
my $result = $session->trap(
-enterprise => $OID,
-generictrap => 6,
#-generictrap => 2,
-specifictrap => 9999,
-varbindlist => [$OID,OCTET_STRING,$ping_ip,
$OID,OCTET_STRING,$wan_ip,
$OID,OCTET_STRING,$loopback_ip,
$OID,OCTET_STRING,$customer_name,
$OID,OCTET_STRING,$lport_string,
]
);
print "\nError in sending trap\n" if (!$result);
$session->close;
return 0;
}
===================================================
The revision of this program uses a database to track info
and has an XML input, deletion mechanisim built in. It is still 'in t
+he works' but runs just fine.
===================================================
#!/usr/bin/perl -w
#######################################################
# RCS INFO #
#######################################################
# $Author: cmjohnso $
# $Date: 2003/10/03 19:19:20 $
# $Revision: 1.9 $
# $Source: /u01/home/cmjohnso/scripts/snmp/new/RCS/mping3.pl,v $
#######################################################
# $Log: mping3.pl,v $
# Revision 1.9 2003/10/03 19:19:20 cmjohnso
# Trap sending now implemented.
# Currently testing with "specifictrap" of 9998.
# The production one is 9999.
#
# Revision 1.8 2003/10/03 15:36:29 cmjohnso
# Adding debugging procedures.
# Figured it was wise to back up first.
#
# Revision 1.7 2003/10/02 21:18:20 cmjohnso
# Added a child process to clean the database.
# It removes all the records over two hours old.
# Might add that to a command line opt.
# Also, worked onthe shutdown method. Seems to work ok,
# but not that great, sometimes does not kill all the
# processes and just hangs then they have to be killed
# from another session. This will need more work, but
# for now will have to do, shouldn't be shut down that
# much anyway.
#
# Revision 1.6 2003/09/30 21:09:26 cmjohnso
# Finally working with the child termination.
# Each child receives the 'USR1' signal
# and ends gracefully when it's work cycle has
# completed, then the parent exits.
#
# Revision 1.5 2003/09/30 15:26:58 cmjohnso
# Load of data, reporting of errors and
# all associated functions working fine
# at this time.
# Will now have to work on the actual DB
# monitoring / reporting /maintaining
# process.
#
# Revision 1.4 2003/09/30 13:18:25 cmjohnso
# *** empty log message ***
#
#######################################################
use strict;
use IO::Select;
use IO::Socket;
use Fcntl;
use POSIX qw(:signal_h WNOHANG);
#use Net::SNMP;
use DBI;
use Sys::Hostname;
use Getopt::Long qw(:config pass_through );
use XML::Simple;
use Data::Dumper;
$|++;
umask(0177);
(my $VERSION = '$Revision: 1.9 $') =~ s/\$(.+) \$/$1/;
my $MAXCHILDREN = 20;
my $inputFile = "./add.xml";
my $deleteFile = "./del.xml";
my $socket_path = "./unix_socket";
my $OID = '.1.3.6.1.4.1.15102.2.1.1.1';
my $DEBUG = 0;
my $dump = 0;
my $run = 0;
my $version = 0;
my %options = ( 'children=i' => \$MAXCHILDREN,
'version' => \$version,
'socket' => \$socket_path,
'debug+' => \$DEBUG,
'input' => \$inputFile,
'del' => \$deleteFile,
'dump' => \$dump,
'run' => \$run,
'<>' => sub {print "Invalid Option:
+$_[0]!\n";exit},
);
my %ip_epochs;
my %failures;
my %child_pids;
GetOptions(%options);
if (${$options{dump}}) {
print $VERSION,"\n";
print "There are $MAXCHILDREN child processes configured\n";
print "Input file is ",${$options{input}},":\n";
print "Delete file is ",${$options{del}},":\n";
print "Comm socket is ",${$options{socket}},":\n";
print "DEBUG Level is ",${$options{'debug+'}},":\n";
exit if ! ${$options{run}};
}
if (${$options{version}}) {
print $VERSION,"\n";
exit if ! ${$options{run}};
}
# Database specific variables
my $add_file = "./add.xml";
my $del_file = "./del.xml";
my $user = 'pingapp';
my $pass = 'manager';
my $dbhost = 'pingapp';
my $dsn = "DBI:mysql:pingapp:pingapp";
my $dbh;
my $sth;
my $query;
my $xml_add;
my $xml_del;
$SIG{TERM} = $SIG{INT} = sub { $SIG{TERM} = $SIG{INT} = sub { print "
+\nPlease be patient, there are ".scalar keys(%child_pids)." child pro
+cesses still running\n"};pprint ("IN $$\n");kill_child();unlink $sock
+et_path; exit 0 };
$dbh = DBI->connect($dsn,$user,$pass,{RaiseError => 1, AutoCommit => 1
+})
or die "Could not connect to database. $DBI::errstr\n";
my $add_time = (stat($add_file))[9];
my $e_add_time =
$dbh->selectrow_array("SELECT EPOCH_TIME FROM INPUT_FILES WHERE FIL
+E_NAME='$add_file'");
my $del_time = (stat($del_file))[9];
my $e_del_time =
$dbh->selectrow_array("SELECT EPOCH_TIME FROM INPUT_FILES WHERE FIL
+E_NAME='$del_file'");
# The section below checks the mtime of the input files
# obtained above stored in $add_time and $deltime
# against the last mtime of each stored in the database.
# if the time has changed it knows to update the
# CLIENT_INFO table accrodingly.
if ( $add_time > $e_add_time )
{
print "Updating add Database.\n";
$dbh->prepare("REPLACE INPUT_FILES VALUES('$add_file','$add_time')"
+)->execute();
$xml_add = XMLin($add_file,NormalizeSpace => 2);
}
if ( $del_time > $e_del_time )
{
print "Updating del Database.\n";
$dbh->prepare("REPLACE INPUT_FILES VALUES('$del_file','$del_time')"
+)->execute();
$xml_del = XMLin($del_file,NormalizeSpace => 2);
}
if ($xml_add)
{
my $values;
for (@{$xml_add->{CLIENT}})
{
$values .= "('$_->{PING_IP}',".
"'$_->{WAN_IP}',".
"'$_->{LOOP_IP}',".
"'$_->{CUST_NM}',".
"'$_->{PVC_NM}'),";
}
chop $values; # removes the last comma (bad SQL syntax)
$dbh->prepare("REPLACE CLIENT_INFO VALUES$values")->execute();
}
if ($xml_del)
{
my $values;
for (@{$xml_del->{CLIENT}})
{
$values .= "'$_->{PING_IP}', ";
}
chop $values; # removes the last space (bad SQL syntax)
chop $values; # removes the last comma (bad SQL syntax)
$dbh->prepare("DELETE FROM CLIENT_INFO WHERE PING_IP IN ($values)")->
+execute();
}
main();
END
{
unlink $socket_path;
}
sub main
{
my $buffer;
my $setsleep = 0;
my $selector = IO::Select->new();
my $listenSocket;
my @sourcearray;
# fork child process to maintain the Failures table;
if (my $pid = fork )
{
$child_pids{$pid} = 1;
#in parent here;
}
elsif (! defined $pid)
{
print "Fork failed for DB cleaner -->$!\n";
exit;
}
else
{
db_cleaner($dsn,$user,$pass);
exit;
}
# fork alerter process to send SNMP alerts;
if (my $pid = fork )
{
$child_pids{$pid} = 1;
#in parent here;
}
elsif (! defined $pid)
{
print "Fork failed for alerter -->$!\n";
exit;
}
else
{
alerter($dsn,$user,$pass);
exit;
}
$listenSocket = mklisten($socket_path);
$selector->add($listenSocket);
load_input_file($dbh, \%ip_epochs);
load_array(\@sourcearray, \%ip_epochs);
# fork child processes to do the ping.
for (my $counter = 1;$counter <= $MAXCHILDREN;$counter++)
{
if (my $pid = fork)
{
$child_pids{$pid} = 1;
# in parent, do nothing
}
elsif (! defined $pid)
{
print "fork failed! -->$!\n";
exit;
}
else
{
my $sigset = POSIX::SigSet->new(SIGINT,SIGTERM,SIGCHLD);
sigprocmask(SIG_BLOCK,$sigset);
close($listenSocket);
ping_child($socket_path,$dsn,$dbhost,$user,$pass);
sigprocmask(SIG_UNBLOCK,$sigset);
exit;
}
}
pprint (join(",",keys(%child_pids)),"\n");
while(1)
{
$SIG{CHLD} = \&reap_child;
#waitpid(-1,&WNOHANG);
my @canread = $selector->can_read();
for (@canread)
{
if ($_ == $listenSocket)
{
my $client = clientAccept($_);
$client->autoflush(1);
$selector->add($client);
next;
}
else
{
sysread($_,$buffer,8192);
chomp $buffer;
if ($buffer eq "req")
{
my $nextline = nextInput(\@sourcearray);
if (defined $nextline)
{
chomp $nextline;
my $time = time() - $ip_epochs{$nextline};
if ( $time < 60 )
{
pprint ("SLEEPING $time seconds....\n");
sleep $time;
}
syswrite($_,$nextline);
$ip_epochs{$nextline} = time();
}
else
{
load_array(\@sourcearray, \%ip_epochs);
syswrite($_,"TRY_AGAIN");
}
}
else
{
syswrite(STDOUT,"INVALID! EXITING!\n");
$selector->remove($_);
close $_;
}
}
}
}
}## END MAIN ##
########
# Child processes.
sub ping_child
{
use Net::Ping;
my $SHUTDOWN = 0;
my $sigset = POSIX::SigSet->new(SIGINT,SIGTERM,SIGCHLD,SIGUSR1);
sigprocmask(SIG_BLOCK,$sigset);
for (keys(%SIG))
{
$SIG{$_} = 'DEFAULT';
}
$SIG{USR1} = sub{ pprint ("Child $$ in SIGNAL\n"); $SHUTDOWN++ ;pp
+rint ("\$SHUTDOWN now $SHUTDOWN\n") };
#my $old_sigset = POSIX::SigSet->new;
my ($sockpath, $dsn,
$host,$user, $pass) = (@_);
my $clientSocket = IO::Socket::UNIX->new($sockpath);
my $ping = Net::Ping->new('icmp',5);
my $buffer;
my $db_handle = DBI->connect($dsn,$user,$pass,{RaiseError => 1, Au
+toCommit => 1})
or die "Could not connect to database. $DBI::errstr\n";
until (defined($clientSocket))
{
ssleep(.250);
$clientSocket = IO::Socket::UNIX->new($sockpath);
}
$clientSocket->autoflush(1);
while (1)
{
pprint ("1\n");
if ($SHUTDOWN) { pprint ("Child $$ received shutdown\n");exit
+0};
pprint ("2\n");
syswrite($clientSocket,"req");
pprint ("3\n");
my $bytes_read = 0;
pprint ("4\n");
my $tmpval;
pprint ("5\n");
if ($SHUTDOWN) { pprint ("Child $$ received shutdown\n");ex
+it 0};
pprint ("6\n");
$bytes_read = sysread($clientSocket,$buffer,8192);
pprint ("7\n");
chomp($buffer);
pprint ("8\n");
$tmpval = $buffer;
pprint ("9\n");
if ($buffer eq "TRY_AGAIN")
{
pprint ("10\n");
pprint ("CHILD $$ TRYING AGAIN\n");
next;
}
pprint ("11\n");
pprint ("IN CHILD $$ PINGING $tmpval\n");
pprint ("12\n");
my $block = POSIX::SigSet->new(SIGINT,SIGTERM,SIGCHLD);
#my $pre_block = POSIX::SigSet->new;
sigprocmask(SIG_BLOCK,$block);
my $pingresult = $ping->ping($tmpval);
sigprocmask(SIG_UNBLOCK,$block);
pprint ("13\n");
if ( (! defined($pingresult)) or (!$pingresult) )
{
pprint ("14\n");
my $EPOCH_TIME = time();
pprint ("15\n");
my $sth = $db_handle->prepare("INSERT INTO FAILURES".
" VALUES(\'$tmpval\',\'$EPOCH_TIME\
+')");
pprint ("16\n");
die "\$sth error: $!\n" if (! $sth);
pprint ("17\n");
pprint (" INSERTING \'$tmpval\' at \'$EPOCH_TIME\'\n"
+);
pprint ("18\n");
my $return = $sth->execute;
pprint ("19\n");
die "\$return error: $!\n" if (! $return);
pprint ("20\n");
}
pprint ("21\n");
}
$db_handle->disconnect();
exit;
}
###
# SUBROUTINES
sub load_array
{
# loads the array passed in with the
# data returned from the database
# handle passed in.
my $arrref = shift;
my $hashref = shift;
my $count = 0;
@$arrref = sort { $$hashref{$b} <=> $$hashref{$a} } ( keys(%
+$hashref)) ;
}
sub load_input_file
{
my $db_handle = shift;
my $hashref = shift;
my $count = 0;
my $sth = $db_handle->selectcol_arrayref("SELECT PING_IP FRO
+M CLIENT_INFO");
map { $$hashref{$_}=$count++ } @$sth;
die "CRITICAL ERROR, cannot continue!\n$count IP's loaded from tab
+le.\n" if (! $count);
}
sub ssleep
{
# sleeps for the number of seconds passed.
# accepts decimal (ex 1/4 sec = .250);
select(undef,undef,undef,shift);
}
###
# Create a listen socket
sub mklisten
{
my $sockpath = shift;
my $listenSocket =
IO::Socket::UNIX->new(Local => $sockpath,
Listen => $MAXCHILDREN + 1,
Type => SOCK_STREAM,
) or die "Socket: $!\n";
return $listenSocket;
}
###
# accept on a passed listen socket
sub clientAccept
{
my $listenSocket = shift;
my $newclient = $listenSocket->accept();
$newclient->autoflush(1);
return $newclient;
}
# Get next line from the input file
sub nextInput
{
my $array = shift;
return shift(@$array);
}
sub shut_down
{
}
sub kill_child
{
pprint ("Please wait, stopping child processes from $$\n");
$SIG{CHLD} = \&reap_child;
my $procs = kill USR1 => keys(%child_pids);
#print "in kill_child\n";
pprint ("Killing $procs child processes\n");
#sleep while (reap_child());
##sleep while (%child_pids);
sleep while (scalar(keys(%child_pids)) );
#print "leaving kill_child\n";
#$SIG{CHLD} = \&reap_child;
#for (keys(%child_pids))
#{
#kill 'USR1', $_;
##reap_child();
#my $pid = waitpid(-1,&WNOHANG);
#delete $child_pids{$pid};
##print "in kill_child\n";
##sleep while (%child_pids);
##print "leaving kill_child\n";
#}
}
sub reap_child
{
pprint ("entered reap_child\n");
while ( (my $child = waitpid(-1,&WNOHANG)) > 0 ) {
pprint ("reap_child: deleting $child\n");
delete $child_pids{$child};
}
}
sub db_cleaner
{
###
# blocks INT AND TERM signals from child;
my $sigset = POSIX::SigSet->new(SIGINT,SIGTERM);
my $old_sigset = POSIX::SigSet->new;
sigprocmask(SIG_BLOCK,$sigset);
#
###
my $SHUTDOWN = 0;
$SIG{USR1} = sub{ $SHUTDOWN++ };
tie (my $time, 'time_now');
my ($dsn,$user,$pass) = (@_);
my $sleep_time = 3600;
my $two_hours = 7200; # two hours in seconds
my $clean_table = "DELETE FROM FAILURES WHERE ".
"EPOCH_TIME < ($time - $two_hours)";
my $dbh = DBI->connect($dsn,$user,$pass,{RaiseError => 1, AutoComm
+it => 1})
or die "Could not connect to database. $DBI::errstr\n";
#while (sleep $sleep_time)
while (1)
{
if ($SHUTDOWN)
{
pprint ("Shutting down DB cleaner\n");
exit 0;
}
$dbh->do($clean_table)
or warn "Could not execute table clean! -->$!\n";
sleep $sleep_time;
}
}
sub alerter
{
use Net::SNMP;
my $SHUTDOWN = 0;
my ($dsn,$user,$pass) = (@_);
my $sleep_time = 60;
tie my $now, 'time_now';
###
# blocks INT AND TERM signals from child;
my $sigset = POSIX::SigSet->new(SIGINT,SIGTERM);
my $old_sigset = POSIX::SigSet->new;
sigprocmask(SIG_BLOCK,$sigset);
$SIG{USR1} = sub{ $SHUTDOWN++ };
#
###
###
# Customer info table & fields used.
my $T_cust = 'CUSTOMER_INFO'; # Customer info table name
my $F_name = 'CUST_NAME';
my $F_P_IP = 'PING_IP';
my $F_W_IP = 'WAN_IP';
my $F_DESC = 'TXT_DESC';
my $T_fail = 'FAILURES'; # Failures table name
my $F_F_IP = 'FAIL_IP';
my $F_etim = 'EPOCH_TIME';
#
###
###
# Alerting thresholds
my $L1_fail = 600; # 10 minutes, in seconds
my $L1_thresh = 7;
my $L2_fail = 1200; # 20 minutes, in seconds
my $L2_thresh = 15;
#
###
###
# Level 1 query;
my $L1_query = "SELECT C.CUST_NAME,C.PING_IP,C.WAN_IP,C.LOOPBACK_
+IP,C.TXT_DESC ".
"FROM CLIENT_INFO C, FAILURES F ".
#"WHERE F.EPOCH_TIME > ($now - 3600) ".
"WHERE F.EPOCH_TIME > ($now - $L1_fail) ".
"AND C.PING_IP=F.FAIL_IP ".
"GROUP BY C.CUST_NAME ".
"HAVING COUNT(C.CUST_NAME) > $L1_thresh ".
"AND COUNT(C.CUST_NAME) < $L2_thresh";
#
###
###
# Level 2 query;
my $L2_query = "SELECT C.CUST_NAME,C.PING_IP,C.WAN_IP,C.LOOPBACK_
+IP,C.TXT_DESC ".
"FROM CLIENT_INFO C, FAILURES F ".
"WHERE F.EPOCH_TIME > ($now - $L2_fail) ".
"AND C.PING_IP=F.FAIL_IP ".
"GROUP BY C.CUST_NAME ".
"HAVING COUNT(C.CUST_NAME) > $L2_thresh ";
#
###
###
# Data base connection
my $db_handle = DBI->connect($dsn,$user,$pass,{RaiseError => 1, A
+utoCommit => 1})
or die "Could not connect to database. $DBI::errs
+tr\n";
#
###
###
# Actual reporting/alerting section
# my ($session,$error) = Net::SNMP->session(
# -hostname => '10.51.9.61' ,
# -community => 'public',
# -port => '162',
# #-hostname => shift || $host,
# #-community => shift || $community,
# #-port => shift || $port,
# );
# die "Error creating the SNMP session\n" if ($error);
my $OID = '.1.3.6.1.4.1.15102.2.1.1.1';
while (1)
{
if ($SHUTDOWN)
{
pprint ("Shutting down alerter from signal\n");
exit;
}
my $L1_ref = $db_handle->selectall_arrayref($L1_query);
my $L2_ref = $db_handle->selectall_arrayref($L2_query);
#print ("L1 failures:\n",Dumper $L1_ref,"-"x20,"\n") if (@$L1_ref
+);
#print ("L2 failures:\n",Dumper $L2_ref,"-"x20,"\n") if (@$L2_ref
+);
if (@$L1_ref)
{
for (@$L1_ref)
{
# send level 1 traps
$$_[4] = 'NULL' if (!$$_[4]);
print "Sending trap on $$_[0]\n";
print "send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],5,$OID)
+\n";
send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],5,$OID);
}
}
if (@$L2_ref)
{
for (@$L2_ref)
{
# send level 2 traps
$$_[4] = 'NULL' if (!$$_[4]);
print "Sending trap on $$_[0]\n";
print "send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],6,$OID)
+\n";
send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],6,$OID);
}
}
#if (@$L2_ref)
#{
# # send level 2 traps
# for ( $$L2_ref[0] )
# {
# $$_[4] = 'NULL' if (!$$_[4]);
# print "Sending trap on $$_[0]\n";
# print "send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],6,$OID
+)\n";
# send_trap($$_[1],$$_[2],$$_[3],$$_[0],$$_[4],6,$OID);
# }
#}
sleep 10;
#sleep 60;
}
$db_handle->disconnect;
exit;
sub send_trap
{
my ($session,$error) = Net::SNMP->session(
-hostname => '66.43.143.104' ,
-community => 'public',
-port => '162',
);
die "Error creating the SNMP session\n" if ($error);
my ($ping_ip,$wan_ip,$loopback_ip,$customer_name,$lport_string,$s
+everity,$OID) = (@_);
my $result = $session->trap(
-enterprise => $OID,
-generictrap => 6,
-specifictrap => 9999,
-varbindlist => [$OID,OCTET_STRING,$ping_ip,
$OID,OCTET_STRING,$wan_ip,
$OID,OCTET_STRING,$loopback_ip,
$OID,OCTET_STRING,$customer_name,
$OID,OCTET_STRING,$lport_string,
$OID,OCTET_STRING,$severity,
]);
print "Error in sending trap\n" if (!$result);
$session->close;
}
}
sub pprint
{
my $statement;
if ($DEBUG)
{
$statement = join("",(@_));
print $statement;
}
}
eval {
package time_now;
sub TIESCALAR {
my ($pkg) = @_;
my $obj = time();
return (bless \$obj, $pkg);
}
sub FETCH {
my ($r_obj) = @_;
return time();
}
1;
}
|