f0b6fde5d9
git-svn-id: https://svn.code.sf.net/p/xcat/code/xcat-core/trunk@12565 8638fb3e-16cb-4fca-ae20-7b5d299a9bcd
1516 lines
43 KiB
Perl
1516 lines
43 KiB
Perl
#!/usr/bin/env perl
|
|
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.htm
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head1 SINV
|
|
|
|
=head2 Package Description
|
|
|
|
This program module file supplies a set of utility programs for
|
|
the sinv command.
|
|
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
package xCAT::SINV;
|
|
BEGIN
|
|
{
|
|
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : -d '/opt/xcat' ? '/opt/xcat' : '/usr';
|
|
}
|
|
use strict;
|
|
use xCAT::MsgUtils;
|
|
use xCAT::NodeRange;
|
|
use xCAT::NodeRange qw/noderange abbreviate_noderange/;
|
|
use xCAT::Utils;
|
|
use Fcntl qw(:flock);
|
|
use Getopt::Long;
|
|
#use Data::Dumper;
|
|
my $tempfile;
|
|
my $errored = 0;
|
|
my @dshresult;
|
|
my $templatepath;
|
|
my $processflg;
|
|
my @cmdresult;
|
|
my @errresult;
|
|
|
|
#
|
|
# Subroutines
|
|
#
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 usage
|
|
Display usage message
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub usage
|
|
{
|
|
my $callback = shift;
|
|
## usage message
|
|
|
|
my $usagemsg1 =
|
|
"The sinv command is designed to check the configuration of nodes in a cluster.\nRun man sinv for more information.\n\nInput parameters are as follows:\n";
|
|
my $usagemsg1a = "sinv -h \nsinv -v \nsinv";
|
|
my $usagemsg3 =
|
|
" -p <template path> [-o output file ] [-t <template count>]\n";
|
|
my $usagemsg4 = " [-r remove templates] [-s <seednode>]\n";
|
|
my $usagemsg5 = " [-e exactmatch] [-i ignore] [-V verbose]\n";
|
|
my $usagemsg6 = " {-c <command> | -f <command file>}";
|
|
my $usagemsg .= $usagemsg1 .= $usagemsg1a .= $usagemsg3 .= $usagemsg4 .=
|
|
$usagemsg5 .= $usagemsg6;
|
|
### end usage mesage
|
|
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = $usagemsg;
|
|
xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
|
|
return;
|
|
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 parse_and_run_sinv
|
|
Checks input arguments and runs sinv from the plugin
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub parse_and_run_sinv
|
|
{
|
|
my ($class, $request, $callback, $sub_req) = @_;
|
|
my $rsp = {};
|
|
my $rc = 0;
|
|
$::CALLBACK = $callback;
|
|
my $args = $request->{arg};
|
|
if (!($args)) {
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"No arguments have been supplied to the sinv command. Check the sinv man page for appropriate input. \n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
|
|
@ARGV = @{$args}; # get arguments
|
|
my %options = ();
|
|
$Getopt::Long::ignorecase = 0; #Checks case in GetOptions
|
|
Getopt::Long::Configure("bundling");
|
|
|
|
if (
|
|
!GetOptions(
|
|
'h|help' => \$options{'help'},
|
|
't|tc=s' => \$options{'template_cnt'},
|
|
'p|tp=s' => \$options{'template_path'},
|
|
'r|remove' => \$options{'remove_template'},
|
|
'o|output=s' => \$options{'output_file'},
|
|
's|seed=s' => \$options{'seed_node'},
|
|
'e|exactmatch' => \$options{'exactmatch'},
|
|
'i|ignorefirst' => \$options{'ignorefirst'},
|
|
'c|cmd=s' => \$options{'sinv_cmd'},
|
|
'f|file=s' => \$options{'sinv_cmd_file'},
|
|
'v|version' => \$options{'version'},
|
|
'V|Verbose' => \$options{'verbose'},
|
|
)
|
|
)
|
|
{
|
|
|
|
&usage($callback);
|
|
exit 1;
|
|
}
|
|
if ($options{'help'})
|
|
{
|
|
&usage($callback);
|
|
exit 0;
|
|
}
|
|
if ($options{'version'})
|
|
{
|
|
my $version = xCAT::Utils->Version();
|
|
$version .= "\n";
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = $version;
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
exit 0;
|
|
}
|
|
if ($options{'verbose'})
|
|
{
|
|
$::VERBOSE = "yes";
|
|
}
|
|
|
|
# if neither command or file, error
|
|
if (!($options{'sinv_cmd'}) && (!($options{'sinv_cmd_file'})))
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"Neither the sinv command, nor the sinv command file have been supplied.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
|
|
# if both command and file, error
|
|
if (($options{'sinv_cmd'}) && (($options{'sinv_cmd_file'})))
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"Both the sinv command, and the sinv command file have been supplied. Only one or the other is allowed.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
|
|
if ( defined( $options{template_path} ) && ($options{template_path} !~ /^\//) ) {#relative path
|
|
$options{template_path} = xCAT::Utils->full_path($options{template_path}, $request->{cwd}->[0]);
|
|
}
|
|
if ( defined( $options{output_file} ) && ($options{output_file} !~ /^\//) ) {#relative path
|
|
$options{output_file} = xCAT::Utils->full_path($options{output_file}, $request->{cwd}->[0]);
|
|
}
|
|
if ( defined( $options{sinv_cmd_file} ) && ($options{sinv_cmd_file} !~ /^\//) ) {#relative path
|
|
$options{sinv_cmd_file} = xCAT::Utils->full_path($options{sinv_cmd_file}, $request->{cwd}->[0]);
|
|
}
|
|
#
|
|
# Get Command to run
|
|
#
|
|
my $cmd;
|
|
if ($options{'sinv_cmd'})
|
|
{
|
|
$cmd = $options{'sinv_cmd'};
|
|
}
|
|
else
|
|
{
|
|
|
|
# read the command from the file
|
|
if (!(-e $options{'sinv_cmd_file'}))
|
|
{ # file does not exist
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"Input command file: $options{'sinv_cmd_file'} does not exist.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
return 1;
|
|
}
|
|
$cmd = `cat $options{'sinv_cmd_file'}`;
|
|
}
|
|
chomp $cmd;
|
|
|
|
#
|
|
# the command can be either xdsh or rinv for now
|
|
# strip off the program and the noderange
|
|
#
|
|
my @nodelist = ();
|
|
my @cmdparts = split(' ', $cmd);
|
|
my $cmdtype = shift @cmdparts;
|
|
my $noderange = shift @cmdparts;
|
|
my @cmd = ();
|
|
if ($noderange =~ /^-/)
|
|
{ # no noderange
|
|
push @cmd, $noderange; # put flag back on command
|
|
}
|
|
foreach my $part (@cmdparts)
|
|
{
|
|
|
|
push @cmd, $part; # build rest of command
|
|
}
|
|
if (($cmdtype ne "xdsh") && ($cmdtype ne "rinv"))
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"Only commands xdsh and rinv are currently supported.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
my $cmdoutput;
|
|
if ($cmdtype eq "xdsh")
|
|
{ # choose output routine to run
|
|
$cmdoutput = "xdshoutput";
|
|
}
|
|
else
|
|
{ # rinv
|
|
$cmdoutput = "rinvoutput";
|
|
}
|
|
|
|
# this must be a noderange or the flag indicating we are going to the
|
|
# install image ( -i) for xdsh, only case where noderange is not required
|
|
|
|
if ($noderange =~ /^-/)
|
|
{ # no noderange, it is a flag
|
|
@nodelist = "NO_NODE_RANGE";
|
|
|
|
# add flag back to arguments
|
|
$args .= $noderange;
|
|
}
|
|
else
|
|
{ # get noderange
|
|
@nodelist = noderange($noderange); # expand noderange
|
|
if (nodesmissed)
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"Invalid or missing noderange:" . join(',', nodesmissed);
|
|
xCAT::MsgUtils->message("E", $rsp, $callback, 1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Get exact match request
|
|
#
|
|
my $exactmatch = "NO";
|
|
if ($options{'exactmatch'})
|
|
{
|
|
$exactmatch = "YES";
|
|
}
|
|
|
|
#
|
|
# Get ignore matches on first template request
|
|
#
|
|
my $ignorefirsttemplate = "NO";
|
|
if ($options{'ignorefirst'})
|
|
{
|
|
$ignorefirsttemplate = "YES";
|
|
}
|
|
|
|
#
|
|
#
|
|
# Get template path
|
|
#
|
|
my $admintemplate;
|
|
$templatepath = $options{'template_path'};
|
|
if (!$templatepath)
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Missing template path on the command.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
else
|
|
{
|
|
if (-e ($templatepath))
|
|
{ # the admin has input the template
|
|
$admintemplate = "YES";
|
|
}
|
|
else
|
|
{
|
|
$admintemplate = "NO";
|
|
}
|
|
}
|
|
|
|
chomp $templatepath;
|
|
|
|
#
|
|
# Get template count
|
|
#
|
|
|
|
my $templatecnt = $options{'template_cnt'};
|
|
if (!$templatecnt)
|
|
{
|
|
$templatecnt = 0; # default
|
|
}
|
|
chomp $templatecnt;
|
|
|
|
#
|
|
# Get remove template value
|
|
#
|
|
|
|
my $rmtemplate = "NO"; #default
|
|
if ($options{'remove_template'})
|
|
{
|
|
$rmtemplate = "YES";
|
|
}
|
|
chomp $rmtemplate;
|
|
|
|
#
|
|
#
|
|
# Get where to put the output
|
|
#
|
|
|
|
my $outputfile = $options{'output_file'};
|
|
if (!$outputfile)
|
|
{
|
|
$::NOOUTPUTFILE = 1;
|
|
}
|
|
else
|
|
{
|
|
|
|
chomp $outputfile;
|
|
}
|
|
|
|
# open the file for writing, if it exists
|
|
if ($outputfile)
|
|
{
|
|
unless (open(OUTPUTFILE, ">$outputfile"))
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = " Cannot open $outputfile for output.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
exit 1;
|
|
}
|
|
$::OUTPUT_FILE_HANDLE = \*OUTPUTFILE;
|
|
|
|
}
|
|
|
|
#
|
|
# For xdsh command
|
|
# Get seed node if it exists to build the original template
|
|
# if seed node does not exist and the admin did not submit a
|
|
# template, the the first node becomes the seed node
|
|
# if there is no nodelist then error
|
|
my @seed;
|
|
my $seednode = $options{'seed_node'};
|
|
if ($seednode)
|
|
{
|
|
chomp $seednode;
|
|
push @seed, $seednode;
|
|
}
|
|
else
|
|
{
|
|
if ($admintemplate eq "NO") # default the seed node
|
|
{ # admin did not generate a template
|
|
if ($nodelist[0] ne "NO_NODE_RANGE")
|
|
{
|
|
push @seed, $nodelist[0]; # assign first element as seed
|
|
$seednode = $nodelist[0];
|
|
}
|
|
else
|
|
{ # error cannot default
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] =
|
|
"No template or seed node supplied and no noderange to choose a default.\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback, 1);
|
|
exit 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
my $tmpnodefile;
|
|
|
|
#
|
|
# Build Output header
|
|
if (($::VERBOSE) || ($outputfile))
|
|
{
|
|
|
|
#
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Command started with following input.\n";
|
|
if ($cmd)
|
|
{
|
|
$rsp->{data}->[1] = "$cmdtype cmd:$cmd.\n";
|
|
}
|
|
else
|
|
{
|
|
$rsp->{data}->[1] = "$cmdtype cmd:None.\n";
|
|
}
|
|
$rsp->{data}->[2] = "Template path:$templatepath.\n";
|
|
$rsp->{data}->[3] = "Template cnt:$templatecnt.\n";
|
|
$rsp->{data}->[4] = "Remove template:$rmtemplate.\n";
|
|
if ($outputfile)
|
|
{
|
|
$rsp->{data}->[5] = "Output file:$outputfile.\n";
|
|
}
|
|
else
|
|
{
|
|
$rsp->{data}->[5] = "Output file:None.\n";
|
|
}
|
|
$rsp->{data}->[6] = "Exactmatch:$exactmatch.\n";
|
|
$rsp->{data}->[7] = "Ignorefirst:$ignorefirsttemplate.\n";
|
|
if ($seednode)
|
|
{
|
|
$rsp->{data}->[8] = "Seed node:$seednode.\n";
|
|
}
|
|
else
|
|
{
|
|
$rsp->{data}->[8] = "Seed node:None.\n";
|
|
}
|
|
if ($options{'sinv_cmd_file'})
|
|
{
|
|
$rsp->{data}->[9] = "file:$options{'sinv_cmd_file'}.\n";
|
|
}
|
|
else
|
|
{
|
|
$rsp->{data}->[9] = "file:None.\n";
|
|
}
|
|
|
|
#write to output file the header
|
|
my $i = 0;
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
while ($i < 10)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[$i];
|
|
$i++;
|
|
}
|
|
print $::OUTPUT_FILE_HANDLE "\n";
|
|
}
|
|
if (!($outputfile))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
}
|
|
|
|
# setup a tempfile for command output
|
|
$tempfile = "/tmp/sinv.$$";
|
|
|
|
#
|
|
# if we are to seed the original template,run the dsh command against the
|
|
# seed node and save in template_path
|
|
# already checked for rinv command above and exited, if seed node
|
|
#
|
|
if ($seednode)
|
|
{
|
|
|
|
# Below code needed to run xdsh or rinv from the plugin
|
|
# and still support a hierarchial xdsh
|
|
# this will run xdsh or rinv with input, return to
|
|
# xdshoutput routine or rinvoutput routine
|
|
# and then return inline after this code.
|
|
|
|
$processflg = "seednode";
|
|
@errresult = ();
|
|
@cmdresult = ();
|
|
$sub_req->(
|
|
{
|
|
command => [$cmdtype],
|
|
node => \@seed,
|
|
arg => [@cmd]
|
|
},
|
|
\&$cmdoutput
|
|
);
|
|
|
|
# write the results to the tempfile after running through xdshcoll
|
|
$rc = &storeresults($callback);
|
|
|
|
}
|
|
$processflg = "node";
|
|
|
|
# Tell them we are running the command
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Running $cmdtype command.\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
|
|
# Below code needed to run xdsh/rinv from the plugin
|
|
# and still support a hierarchial xdsh
|
|
# this will run the command with input, return to cmdoutput routine
|
|
# and then return inline after this code.
|
|
@errresult = ();
|
|
@cmdresult = ();
|
|
$sub_req->(
|
|
{
|
|
command => [$cmdtype],
|
|
node => \@nodelist,
|
|
arg => [@cmd]
|
|
},
|
|
\&$cmdoutput
|
|
);
|
|
|
|
|
|
# write the results to the tempfile after running through xdshcoll
|
|
$rc = &storeresults($callback);
|
|
|
|
# Build report and write to output file
|
|
# if file exist and has something in it
|
|
if ((-e $tempfile) && ($rc == 0))
|
|
{ # if cmd returned something
|
|
|
|
# Tell them we are building the report
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Building Report.\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
|
|
xCAT::SINV->buildreport(
|
|
$outputfile, $tempfile,
|
|
$templatepath, $templatecnt,
|
|
$rmtemplate, \@nodelist,
|
|
$callback, $ignorefirsttemplate,
|
|
$exactmatch, $admintemplate
|
|
);
|
|
}
|
|
else
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "No output from $cmdtype.\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
|
|
# Finally we need to cleanup and exit
|
|
#
|
|
if (-e $tempfile)
|
|
{
|
|
|
|
system("/bin/rm $tempfile");
|
|
}
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Command Complete.";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
close(OUTPUTFILE);
|
|
$rsp->{data}->[0] = "Check report in $outputfile.\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
return $rc;
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 buildreport (note originally written only for xdsh but
|
|
now supports rinv also)
|
|
|
|
This routine will take the input template and compare against
|
|
the output of the dsh command and build a report of the differences.
|
|
Read a nodes worth of data from the dsh command
|
|
Call compareoutput- compares the dsh to the template
|
|
Get the template and nodename returned from compareoutput and build hash
|
|
Call writereport to take the hash and write the report to the output file
|
|
Cleanup
|
|
Input (report file, file containing dsh run,template file,template count
|
|
whether to remove the generated templates, original dsh node list,
|
|
ignorefirsttemplate,exactmatch)
|
|
|
|
If exactmatch is chosen, a diff is done against the template and the output.
|
|
If not exactmatch, then each record (line) in the template must be
|
|
checked against the node's output to determine, if it exists.
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub buildreport
|
|
{
|
|
|
|
my (
|
|
$class, $outputfile, $dshrun,
|
|
$templatepath, $templatecnt, $removetemplate,
|
|
$nodelistin, $callback, $ignorefirsttemplate,
|
|
$exactmatch, $admintemplate
|
|
)
|
|
= @_;
|
|
my @nodelist = @$nodelistin;
|
|
my $pname = "buildreport";
|
|
my $rc = $::OK;
|
|
my $rsp = {};
|
|
|
|
# Compare files and build report of nodes that match and those that do not
|
|
if (!-f "$templatepath") # we supplied a template
|
|
{ # does it exist
|
|
$rsp->{data}->[0] = "$templatepath does not exist\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
return;
|
|
|
|
}
|
|
if (!-f "$dshrun")
|
|
{ # does it exist
|
|
$rsp->{data}->[0] = "$dshrun does not exist\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
return;
|
|
}
|
|
|
|
#
|
|
# Build an array of template name
|
|
#
|
|
my @templatearray;
|
|
my $i = $templatecnt;
|
|
push @templatearray, $templatepath; # push first template
|
|
for ($i = 0 ; $i <= $templatecnt ; $i++)
|
|
{
|
|
if ($i != 0)
|
|
{ # more template file to read
|
|
my $templatename = $templatepath . "_" . $i;
|
|
push @templatearray, "$templatename";
|
|
}
|
|
}
|
|
|
|
# Read the output of the dsh or rinv command
|
|
|
|
if (!open(DSHRESULTS, "<$dshrun"))
|
|
{
|
|
$rsp->{data}->[0] = "Error reading: $dshrun\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
return;
|
|
}
|
|
my @dsharray = <DSHRESULTS>;
|
|
close(TEMPLATE);
|
|
close(DSHRESULTS);
|
|
|
|
#Now we have to analyze the template(s) against the dsh command output
|
|
#The matching nodes will be built in one array and the non matching node
|
|
#In another array
|
|
#
|
|
|
|
#
|
|
# For each node entry for the dsh command
|
|
# Build an array of that node's data
|
|
# Compare that node's data to the template
|
|
# Put node in array that matches template or other if no match
|
|
#
|
|
my $match = 0;
|
|
my $host;
|
|
my $label;
|
|
my $headerlines = 0;
|
|
my @processNodearray;
|
|
my $hostline;
|
|
my $nodename;
|
|
my $template;
|
|
my @Nodearray;
|
|
my $dshline;
|
|
my %nodehash;
|
|
DSHARRAY: foreach $dshline (@dsharray) # each line returned from dsh/rinv
|
|
{
|
|
|
|
if ($dshline =~ /============/) # Host header
|
|
{
|
|
if ($headerlines < 2)
|
|
{
|
|
|
|
# read until we reach another header
|
|
push @Nodearray, $dshline;
|
|
$headerlines++;
|
|
}
|
|
else
|
|
{ # Hit next header, process current array
|
|
@processNodearray = @Nodearray; # save node data
|
|
@Nodearray = (); # initialize array
|
|
$headerlines = 1; # already read one line
|
|
push @Nodearray, $dshline; # save next data
|
|
my @info;
|
|
if ($exactmatch eq "YES")
|
|
{ # output matches exactly
|
|
@info =
|
|
xCAT::SINV->diffoutput($outputfile, \@templatearray,
|
|
\@processNodearray, \%nodehash, $callback);
|
|
}
|
|
else
|
|
{ # output is contained in the template
|
|
@info =
|
|
xCAT::SINV->compareoutput($outputfile, \@templatearray,
|
|
\@processNodearray, \%nodehash, $callback);
|
|
}
|
|
$nodename = pop @info;
|
|
$template = pop @info;
|
|
if ($nodename ne "UNKNOWN")
|
|
{
|
|
push @{$nodehash{$template}}, $nodename; # add node name
|
|
} # to template hash
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
if ($dshline !~ /^\s*$/) # skip blanks
|
|
|
|
# skip blanks and stop on the next host
|
|
{
|
|
push @Nodearray, $dshline; # build the node results
|
|
}
|
|
}
|
|
|
|
} # end foreach dshline
|
|
# process the last entry
|
|
if (@Nodearray)
|
|
{
|
|
my @info;
|
|
if ($exactmatch eq "YES")
|
|
{ # output matches exactly
|
|
@info =
|
|
xCAT::SINV->diffoutput(
|
|
$outputfile, \@templatearray,
|
|
\@Nodearray, \%nodehash,
|
|
$callback
|
|
);
|
|
}
|
|
else
|
|
{ # output is contained in the template
|
|
@info =
|
|
xCAT::SINV->compareoutput(
|
|
$outputfile, \@templatearray,
|
|
\@Nodearray, \%nodehash,
|
|
$callback
|
|
);
|
|
}
|
|
$nodename = pop @info;
|
|
$template = pop @info;
|
|
if ($nodename ne "UNKNOWN")
|
|
{
|
|
push @{$nodehash{$template}}, $nodename;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Write the report
|
|
#
|
|
|
|
xCAT::SINV->writereport($outputfile, \%nodehash, \@nodelist, $callback,
|
|
$ignorefirsttemplate);
|
|
|
|
#
|
|
# Cleanup the template files if the remove option was yes
|
|
#
|
|
$removetemplate =~ tr/a-z/A-Z/; # convert to upper
|
|
if ($removetemplate eq "YES")
|
|
{
|
|
foreach $template (@templatearray) # for each template
|
|
{
|
|
if (-f "$template")
|
|
{
|
|
|
|
if (($template ne $templatepath) || ($admintemplate eq "NO"))
|
|
|
|
# not the first template or the first one was not created by
|
|
# admin, it was generated by the code
|
|
{
|
|
`/bin/rm -f $template 2>&1`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 compareoutput
|
|
The purpose of this routine is to build sets of nodes
|
|
that have the same configuration. We will build up
|
|
to template_cnt sets. If more nodes are not part of these
|
|
sets they will be put in an other list.
|
|
|
|
foreach template
|
|
Open the input template
|
|
Compare the template to the input node data
|
|
if match
|
|
add the node to the matched template hash
|
|
end foreach
|
|
if no match
|
|
if generate and a new template allowed
|
|
make this nodes information into a new template
|
|
add the node to matched template
|
|
else
|
|
add the node to "notemplate" list
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub compareoutput
|
|
{
|
|
my ($class, $outputfile, $template_array, $Node_array, $Node_hash,
|
|
$callback) = @_;
|
|
my @Nodearray = @$Node_array;
|
|
my @templatearray = @$template_array;
|
|
my %nodehash = %$Node_hash;
|
|
my $pname = "compareoutput";
|
|
my $rc = $::OK;
|
|
my $templateline;
|
|
my $info;
|
|
my $nodeline;
|
|
my $match = 0;
|
|
my @info;
|
|
my $nodename;
|
|
my @nodenames;
|
|
my @tmpnodenames;
|
|
my $line;
|
|
%nodehash = ();
|
|
my $template;
|
|
my $matchedtemplate;
|
|
my $rsp = {};
|
|
|
|
foreach $template (@templatearray) # for each template
|
|
{
|
|
my $skiphostline = 1;
|
|
if (-f "$template")
|
|
{ # if it exists
|
|
|
|
# Read the template file
|
|
open(TEMPLATE, "<$template");
|
|
my @template = <TEMPLATE>;
|
|
|
|
# now compare host data to template
|
|
foreach $templateline (@template) # for each line in the template
|
|
{
|
|
|
|
# skip the header and blanks
|
|
if ($templateline =~ /============/)
|
|
{ # Host header
|
|
next;
|
|
}
|
|
if ($templateline =~ /UNKNOWN/)
|
|
{ # skip UNKNOWN header
|
|
next;
|
|
}
|
|
if ($skiphostline == 1)
|
|
{
|
|
$skiphostline = 0;
|
|
next;
|
|
}
|
|
if ($templateline !~ /^\s*$/) # skip blanks
|
|
{
|
|
$match = 0;
|
|
my $gothosts = 0;
|
|
foreach $nodeline (@Nodearray) # for each node line
|
|
{
|
|
if ($nodeline =~ /==========/)
|
|
{
|
|
next;
|
|
}
|
|
|
|
if ($gothosts == 0)
|
|
{ # get the hostnames
|
|
$nodename = $nodeline;
|
|
$nodename =~ s/\s*//g; # remove blanks
|
|
chomp $nodename;
|
|
if ($nodename eq "UNKNOWN")
|
|
{ # skip this node
|
|
@info[0] = "NONE";
|
|
@info[1] = "UNKNOWN";
|
|
return @info;
|
|
}
|
|
$gothosts = 1;
|
|
}
|
|
else
|
|
{
|
|
if ($nodeline eq $templateline) # if we find a match
|
|
{ # get out
|
|
$match = 1;
|
|
$matchedtemplate = $template; # save name
|
|
last;
|
|
}
|
|
}
|
|
} # end foreach nodeline
|
|
if ($match == 0)
|
|
{
|
|
last; # had a template line not found
|
|
}
|
|
|
|
} # if header
|
|
} # end foreach templateline
|
|
}
|
|
|
|
# end check exists
|
|
#
|
|
# if match found, process no more templates
|
|
#
|
|
if ($match == 1)
|
|
{
|
|
last; # exit template loop
|
|
}
|
|
}
|
|
|
|
# end foreach template
|
|
#
|
|
# if no match
|
|
# if generate a new template ( check the list of template file
|
|
# to see if there is one that does not exist
|
|
# put node data to new template file
|
|
#
|
|
if ($match == 0)
|
|
{
|
|
my $nodesaved = 0;
|
|
foreach $template (@templatearray)
|
|
{
|
|
if (!-f "$template")
|
|
{
|
|
if (!open(NEWTEMPLATE, ">$template"))
|
|
{
|
|
$rsp->{data}->[0] = "Error opening $template:\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
print NEWTEMPLATE @Nodearray; # build a new template
|
|
$nodesaved = 1;
|
|
close(NEWTEMPLATE);
|
|
$matchedtemplate = $template;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
if ($nodesaved == 0)
|
|
{ # out of templates
|
|
$matchedtemplate = "no template"; # put in other list
|
|
}
|
|
}
|
|
@info[0] = $matchedtemplate;
|
|
@info[1] = $nodename;
|
|
return @info;
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 diffoutput
|
|
The purpose of this routine is to build sets of nodes
|
|
that have the same configuration. We will build up
|
|
to template_cnt sets. If more nodes are not part of these
|
|
sets they will be put in an other list.
|
|
|
|
foreach template
|
|
Open the input template
|
|
Compare the template to the input node data
|
|
if exact match
|
|
add the node to the matched template hash
|
|
end foreach
|
|
if no match
|
|
if generate and a new template allowed
|
|
make this nodes information into a new template
|
|
add the node to matched template
|
|
else
|
|
add the node to "notemplate" list
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub diffoutput
|
|
{
|
|
my ($class, $outputfile, $template_array, $Node_array, $Node_hash,
|
|
$callback) = @_;
|
|
my @Nodearray = @$Node_array;
|
|
my @templatearray = @$template_array;
|
|
my %nodehash = %$Node_hash;
|
|
my $pname = "compareoutput";
|
|
my $rc = $::OK;
|
|
my $templateline;
|
|
my $info;
|
|
my $nodeline;
|
|
my $match = 0;
|
|
my @info;
|
|
my $nodename;
|
|
my $line;
|
|
%nodehash = ();
|
|
my $template;
|
|
my $matchedtemplate;
|
|
my $rsp = {};
|
|
my @template_noheader = ();
|
|
my @nodearray_noheader = ();
|
|
my $hostfound = 0;
|
|
|
|
# build a node array without the header
|
|
# skip any UNKNOWN entries added by xdshcoll
|
|
foreach $nodeline (@Nodearray) # for each node line
|
|
{
|
|
if ($nodeline =~ /================/)
|
|
{ # skip
|
|
next;
|
|
}
|
|
if ($hostfound == 0)
|
|
{ # save the hostname
|
|
$nodename = $nodeline;
|
|
$nodename =~ s/\s*//g; # remove blanks
|
|
chomp $nodename;
|
|
if ($nodename eq "UNKNOWN")
|
|
{ # skip this node
|
|
@info[0] = "NONE";
|
|
@info[1] = "UNKNOWN";
|
|
return @info;
|
|
}
|
|
$hostfound = 1;
|
|
next;
|
|
|
|
}
|
|
|
|
# build node array with no header
|
|
push(@nodearray_noheader, $nodeline);
|
|
} # end foreach nodeline
|
|
|
|
#
|
|
# foreach template
|
|
# build a template array with no header
|
|
# compare to the node array with no header
|
|
#
|
|
|
|
foreach $template (@templatearray) # for each template
|
|
{
|
|
|
|
if (-f "$template")
|
|
{ # if it exists
|
|
my $skiphostline = 1;
|
|
|
|
# Read the template file
|
|
open(TEMPLATE, "<$template");
|
|
my @template = <TEMPLATE>;
|
|
|
|
# now compare host data to template
|
|
foreach $templateline (@template) # for each line in the template
|
|
{
|
|
|
|
# skip the header and blanks
|
|
if ($templateline =~ /============/)
|
|
{ # Host header
|
|
next;
|
|
}
|
|
if ($templateline =~ /UNKNOWN/)
|
|
{ # skip UNKNOWN HEADER
|
|
next;
|
|
}
|
|
if ($skiphostline == 1)
|
|
{
|
|
$skiphostline = 0;
|
|
next;
|
|
}
|
|
if ($templateline !~ /^\s*$/) # skip blanks
|
|
|
|
{
|
|
|
|
# Build template array with no header
|
|
push(@template_noheader, $templateline);
|
|
|
|
} # if header
|
|
} # end foreach templateline
|
|
|
|
# if nodearray matches template exactly,quit processing templates
|
|
my $are_equal =
|
|
compare_arrays(\@nodearray_noheader, \@template_noheader);
|
|
if ($are_equal)
|
|
{
|
|
$matchedtemplate = $template;
|
|
$match = 1;
|
|
last;
|
|
}
|
|
else # go to next template
|
|
{
|
|
$match = 0;
|
|
@template_noheader = ();
|
|
}
|
|
} # end template exist
|
|
|
|
} #end foreach template
|
|
|
|
#
|
|
# if no match
|
|
# if generate a new template - check the list of template files
|
|
# to see if there is one that does not exist
|
|
# put node data to new template file
|
|
#
|
|
if ($match == 0)
|
|
{
|
|
my $nodesaved = 0;
|
|
foreach $template (@templatearray)
|
|
{
|
|
if (!-f "$template")
|
|
{
|
|
if (!open(NEWTEMPLATE, ">$template"))
|
|
{
|
|
$rsp->{data}->[0] = "Error opening $template:\n";
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
print NEWTEMPLATE @Nodearray; # build a new template
|
|
$nodesaved = 1;
|
|
close(NEWTEMPLATE);
|
|
$matchedtemplate = $template;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
if ($nodesaved == 0)
|
|
{ # out of templates
|
|
$matchedtemplate = "no template"; # put in other list
|
|
}
|
|
}
|
|
@info[0] = $matchedtemplate;
|
|
@info[1] = $nodename;
|
|
return @info;
|
|
}
|
|
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 writereport
|
|
|
|
The purpose of this routine is to write the report to the output file
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub writereport
|
|
{
|
|
my ($class, $outputfile, $Node_hash, $nodelistin, $callback,
|
|
$ignorefirsttemplate)
|
|
= @_;
|
|
my %nodehash = %$Node_hash;
|
|
my @dshnodearray = @$nodelistin;
|
|
my $pname = "writereport";
|
|
my $template;
|
|
my @nodenames;
|
|
my @nodearray;
|
|
|
|
#
|
|
# Header message
|
|
#
|
|
my $rsp = {};
|
|
$ignorefirsttemplate =~ tr/a-z/A-Z/; # convert to upper
|
|
my $firstpass = 0;
|
|
my @allnodearray=();
|
|
foreach my $template (sort keys %nodehash)
|
|
{
|
|
|
|
# print template name
|
|
$rsp->{data}->[0] = "The following nodes match $template:\n";
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[0];
|
|
}
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
|
|
#print list of nodes
|
|
@nodenames = @{$nodehash{$template}};
|
|
my $nodelist = "";
|
|
@nodearray=();
|
|
foreach my $nodenameline (@nodenames)
|
|
{
|
|
|
|
# split apart the list of nodes
|
|
my @longnodenames = split(',', $nodenameline);
|
|
foreach my $node (@longnodenames)
|
|
{
|
|
my @shortnodename = split(/\./, $node);
|
|
push @nodearray, $shortnodename[0]; # add to process list
|
|
push @allnodearray, $shortnodename[0]; # add to total list
|
|
$nodelist .= $shortnodename[0]; # add to print list
|
|
$nodelist .= ',';
|
|
}
|
|
}
|
|
|
|
chop $nodelist;
|
|
# convert to noderanges if possible
|
|
my $nodearray;
|
|
$nodearray->{0} = \@nodearray;
|
|
my $newnodelist = abbreviate_noderange($nodearray->{0});
|
|
if ($ignorefirsttemplate ne "YES")
|
|
{ # report first template
|
|
$rsp->{data}->[0] = "$newnodelist\n";
|
|
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[0];
|
|
}
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
}
|
|
else
|
|
{ # do not report nodes on first template
|
|
if ($firstpass == 0)
|
|
{
|
|
$rsp->{data}->[0] =
|
|
"Not reporting matches on first template.\n";
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[0];
|
|
}
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
$firstpass = 1;
|
|
}
|
|
}
|
|
$ignorefirsttemplate = "NO"; # reset for remaining templates
|
|
}
|
|
|
|
#
|
|
# Now check to see if we covered all nodes in the dsh
|
|
# short names must match long names, ignore NO_NODE_RANGE
|
|
#
|
|
my $nodefound = 0;
|
|
my $rsp = {};
|
|
foreach my $dshnodename (@dshnodearray)
|
|
{
|
|
if (($dshnodename ne "NO_NODE_RANGE") && ($dshnodename ne "UNKNOWN"))
|
|
{ # skip it
|
|
my @shortdshnodename;
|
|
my @shortnodename;
|
|
chomp $dshnodename;
|
|
$dshnodename =~ s/\s*//g; # remove blanks
|
|
#foreach my $nodename (@nodearray)
|
|
foreach my $nodename (@allnodearray)
|
|
{
|
|
@shortdshnodename = split(/\./, $dshnodename);
|
|
@shortnodename = split(/\./, $nodename);
|
|
|
|
if ($shortdshnodename[0] eq $shortnodename[0])
|
|
{
|
|
$nodefound = 1; # we have a match
|
|
last;
|
|
}
|
|
}
|
|
if ($nodefound == 0)
|
|
{ # dsh node name missing
|
|
|
|
# add missing node
|
|
$rsp->{data}->[0] .= $shortdshnodename[0];
|
|
$rsp->{data}->[0] .= ",";
|
|
}
|
|
}
|
|
}
|
|
if ($rsp->{data}->[0])
|
|
{
|
|
$rsp->{data}->[0] = "The following nodes had no output:\n";
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[0];
|
|
}
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
chop $rsp->{data}->[0];
|
|
$rsp->{data}->[0] .= "\n";
|
|
if ($::OUTPUT_FILE_HANDLE)
|
|
{
|
|
print $::OUTPUT_FILE_HANDLE $rsp->{data}->[0];
|
|
}
|
|
if (($::VERBOSE) || ($::NOOUTPUTFILE))
|
|
{
|
|
xCAT::MsgUtils->message("I", $rsp, $callback);
|
|
}
|
|
}
|
|
$nodefound = 0;
|
|
return;
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 xdshoutput
|
|
|
|
Check xdsh output - get output from command and pipe to xdshcoll
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub xdshoutput
|
|
{
|
|
my $rsp = shift;
|
|
|
|
my $rc = 0;
|
|
|
|
# Handle info structure, like xdsh returns
|
|
if ($rsp->{warning})
|
|
{
|
|
foreach (@{$rsp->{warning}})
|
|
{
|
|
my $line = $_;
|
|
$line .= "\n";
|
|
push(@errresult, $line);
|
|
}
|
|
}
|
|
if ($rsp->{error})
|
|
{
|
|
foreach (@{$rsp->{error}})
|
|
{
|
|
my $line = $_;
|
|
$line .= "\n";
|
|
push(@errresult, $line);
|
|
}
|
|
}
|
|
if ($rsp->{info})
|
|
{
|
|
foreach (@{$rsp->{info}})
|
|
{
|
|
my $line = $_;
|
|
$line .= "\n";
|
|
push(@cmdresult, $line);
|
|
}
|
|
}
|
|
if ($rsp->{data})
|
|
{
|
|
foreach (@{$rsp->{data}})
|
|
{
|
|
my $line = $_;
|
|
$line .= "\n";
|
|
push(@cmdresult, $line);
|
|
}
|
|
}
|
|
|
|
return $rc;
|
|
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 rinvoutput
|
|
|
|
Check rinv output - get output from command
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
sub rinvoutput
|
|
{
|
|
my $rsp = shift;
|
|
#print "I am here \n";
|
|
#print Dumper($rsp);
|
|
# Handle node structure, like rinv returns
|
|
my $errflg = 0;
|
|
|
|
#if (scalar @{$rsp->{node}})
|
|
if ($rsp->{node})
|
|
{
|
|
|
|
my $nodes = ($rsp->{node});
|
|
my $node;
|
|
foreach $node (@$nodes)
|
|
{
|
|
my $desc = $node->{name}->[0];
|
|
if ($node->{errorcode})
|
|
{
|
|
if (ref($node->{errorcode}) eq 'ARRAY')
|
|
{
|
|
foreach my $ecode (@{$node->{errorcode}})
|
|
{
|
|
$xCAT::Client::EXITCODE |= $ecode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$xCAT::Client::EXITCODE |= $node->{errorcode};
|
|
} # assume it is a non-reference scalar
|
|
}
|
|
if ($node->{error})
|
|
{
|
|
$desc .= ": Error: " . $node->{error}->[0];
|
|
$errflg = 1;
|
|
}
|
|
if ($node->{data})
|
|
{
|
|
if (ref(\($node->{data}->[0])) eq 'SCALAR')
|
|
{
|
|
$desc = $desc . ": " . $node->{data}->[0];
|
|
}
|
|
else
|
|
{
|
|
if ($node->{data}->[0]->{desc})
|
|
{
|
|
$desc = $desc . ": " . $node->{data}->[0]->{desc}->[0];
|
|
}
|
|
if ($node->{data}->[0]->{contents})
|
|
{
|
|
$desc = "$desc: " . $node->{data}->[0]->{contents}->[0];
|
|
}
|
|
}
|
|
}
|
|
if ($desc)
|
|
{
|
|
|
|
my $line = $desc;
|
|
$line .= "\n";
|
|
|
|
push(@cmdresult, $line);
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head3 storeresults
|
|
|
|
Runs command output through xdshcoll and stores in /tmp/<tempfile>
|
|
store results in $tempfile or $templatepath ( for seed node) based on
|
|
$processflag = seednode
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
sub storeresults
|
|
{
|
|
my $callback = shift;
|
|
|
|
# open file to write results of xdsh or rinv command
|
|
my $newtempfile = $tempfile;
|
|
$newtempfile .= "temp";
|
|
open(FILE, ">$newtempfile");
|
|
if ($? > 0)
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Could not open $newtempfile\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
return 1;
|
|
}
|
|
foreach my $line (@cmdresult)
|
|
{
|
|
print FILE $line;
|
|
}
|
|
close FILE;
|
|
my $outputfile;
|
|
if ($processflg eq "seednode")
|
|
{ # cmd to seednode
|
|
$outputfile = $templatepath;
|
|
}
|
|
else
|
|
{ # cmd to nodelist
|
|
$outputfile = $tempfile;
|
|
}
|
|
|
|
# open file to put results of xdshcoll
|
|
open(FILE, ">$outputfile");
|
|
if ($? > 0)
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Could not open $outputfile\n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
return 1;
|
|
}
|
|
my $cmd = " $::XCATROOT/sbin/xdshcoll <$newtempfile |";
|
|
|
|
open(XCOLL, "$cmd");
|
|
if ($? > 0)
|
|
{
|
|
my $rsp = {};
|
|
$rsp->{data}->[0] = "Could not call xdshcoll \n";
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
return 1;
|
|
}
|
|
|
|
my $line;
|
|
|
|
while (<XCOLL>)
|
|
{
|
|
$line = $_;
|
|
print FILE $line
|
|
|
|
}
|
|
|
|
close(XCOLL);
|
|
close FILE;
|
|
|
|
system("/bin/rm $newtempfile");
|
|
|
|
# capture errors
|
|
#
|
|
if (@errresult)
|
|
{ # if errors
|
|
my $rsp = {};
|
|
my $i = 0;
|
|
foreach my $line (@errresult)
|
|
{
|
|
$rsp->{data}->[$i] = "$line";
|
|
$i++;
|
|
}
|
|
xCAT::MsgUtils->message("E", $rsp, $callback);
|
|
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub compare_arrays
|
|
{
|
|
my ($first, $second) = @_;
|
|
return 0 unless @$first == @$second;
|
|
for (my $i = 0 ; $i < @$first ; $i++)
|
|
{
|
|
return 0 if $first->[$i] ne $second->[$i];
|
|
}
|
|
return 1;
|
|
}
|
|
1;
|