2960 lines
99 KiB
Perl
2960 lines
99 KiB
Perl
#!/usr/bin/env perl -w
|
|
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
|
|
#####################################################
|
|
#
|
|
# xCAT plugin package to handle rolling updates
|
|
#
|
|
#####################################################
|
|
|
|
package xCAT_plugin::rollupdate;
|
|
|
|
BEGIN
|
|
{
|
|
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
|
|
}
|
|
|
|
require xCAT::NodeRange;
|
|
require xCAT::Table;
|
|
require xCAT::Utils;
|
|
require xCAT::TableUtils;
|
|
require Data::Dumper;
|
|
require Getopt::Long;
|
|
require xCAT::MsgUtils;
|
|
require File::Path;
|
|
use Text::Balanced qw(extract_bracketed);
|
|
use Safe;
|
|
my $evalcpt = new Safe;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
#
|
|
# Globals
|
|
#
|
|
$::LOGDIR="/var/log/xcat";
|
|
$::LOGFILE="rollupdate.log";
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head1 rollupdate
|
|
|
|
This program module file supports the cluster rolling update functions.
|
|
|
|
Supported commands:
|
|
rollupdate - Create scheduler job command files and submit the jobs
|
|
runrollupdate - Reboot the updategroup in response to request from scheduler
|
|
job
|
|
|
|
If adding to this file, please take a moment to ensure that:
|
|
|
|
1. Your contrib has a readable pod header describing the purpose and use of
|
|
the subroutine.
|
|
|
|
2. Your contrib is under the correct heading and is in alphabetical order
|
|
under that heading.
|
|
|
|
3. You have run tidypod on this file and saved the html file
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
=head2 Cluster Rolling Update
|
|
|
|
=cut
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 handled_commands
|
|
|
|
Return a list of commands handled by this plugin
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub handled_commands {
|
|
return {
|
|
rollupdate => "rollupdate",
|
|
runrollupdate => "rollupdate"
|
|
};
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 preprocess_request
|
|
|
|
|
|
Arguments:
|
|
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub preprocess_request {
|
|
|
|
# set management node as server for all requests
|
|
# any requests sent to service node need to get
|
|
# get sent up to the MN
|
|
|
|
my $req = shift;
|
|
#if already preprocessed, go straight to request
|
|
if ( (defined($req->{_xcatpreprocessed}))
|
|
&& ($req->{_xcatpreprocessed}->[0] == 1))
|
|
{
|
|
return [$req];
|
|
}
|
|
|
|
$req->{_xcatdest} = xCAT::TableUtils->get_site_Master();
|
|
return [$req];
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 process_request
|
|
|
|
Process the rolling update commands
|
|
|
|
Arguments:
|
|
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub process_request {
|
|
$::request = shift;
|
|
$::CALLBACK = shift;
|
|
$::SUBREQ = shift;
|
|
my $ret;
|
|
|
|
# globals used by all subroutines.
|
|
$::command = $::request->{command}->[0];
|
|
$::args = $::request->{arg};
|
|
$::stdindata = $::request->{stdin}->[0];
|
|
|
|
# figure out which cmd and call the subroutine to process
|
|
if ( $::command eq "rollupdate" ) {
|
|
$ret = &rollupdate($::request);
|
|
}
|
|
elsif ( $::command eq "runrollupdate" ) {
|
|
$ret = &runrollupdate($::request);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 rollupdate_usage
|
|
|
|
Arguments:
|
|
Returns:
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
# display the usage
|
|
sub rollupdate_usage {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"\nUsage: rollupdate - Submit cluster rolling update jobs \n";
|
|
push @{ $rsp->{data} }, " rollupdate [-h | --help | -?] \n";
|
|
push @{ $rsp->{data} },
|
|
" rollupdate [-V | --verbose] [-v | --version] [-t | --test] \n ";
|
|
push @{ $rsp->{data} },
|
|
" <STDIN> - stanza file, see /opt/xcat/share/xcat/rollupdate/rollupdate.input.sample";
|
|
push @{ $rsp->{data} }, " for example \n";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
return 0;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 runrollupdate_usage
|
|
|
|
Arguments:
|
|
Returns:
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
# display the usage
|
|
sub runrollupdate_usage {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"\nUsage: runrollupdate - Run submitted cluster rolling update job \n";
|
|
push @{ $rsp->{data} }, " runrollupdate [-h | --help | -?] \n";
|
|
push @{ $rsp->{data} },
|
|
" runrollupdate [-V | --verbose] [-v | --version] scheduler datafile \n ";
|
|
push @{ $rsp->{data} },
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
return 0;
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 processArgs
|
|
|
|
Process the command line and any input files provided on cmd line.
|
|
|
|
Arguments:
|
|
|
|
Returns:
|
|
0 - OK
|
|
1 - just print usage
|
|
2 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub processArgs {
|
|
my $gotattrs = 0;
|
|
|
|
if ( defined ($::args) && @{$::args} ) {
|
|
@ARGV = @{$::args};
|
|
}
|
|
else {
|
|
|
|
# return 2; # can run with no args right now
|
|
}
|
|
|
|
# if (scalar(@ARGV) <= 0) {
|
|
# return 2;
|
|
# }
|
|
|
|
# parse the options
|
|
# options can be bundled up like -vV, flag unsupported options
|
|
Getopt::Long::Configure( "bundling", "no_ignore_case", "no_pass_through" );
|
|
Getopt::Long::GetOptions(
|
|
'help|h|?' => \$::opt_h,
|
|
'test|t' => \$::opt_t,
|
|
'verbose|V' => \$::opt_V,
|
|
'version|v' => \$::opt_v,
|
|
);
|
|
|
|
# Option -h for Help
|
|
# if user specifies "-t" & "-h" they want a list of valid attrs
|
|
if ( defined($::opt_h) ) {
|
|
return 2;
|
|
}
|
|
|
|
# opt_t not yet supported
|
|
if ( defined($::opt_t) ) {
|
|
#$::test = 1;
|
|
$::TEST = 1;
|
|
# my $rsp;
|
|
# push @{ $rsp->{data} }, "The \'-t\' option is not yet implemented.";
|
|
# xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
# return 2;
|
|
}
|
|
|
|
# Option -v for version
|
|
if ( defined($::opt_v) ) {
|
|
my $rsp;
|
|
my $version = xCAT::Utils->Version();
|
|
push @{ $rsp->{data} }, "$::command - $version\n";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
return 1; # no usage - just exit
|
|
}
|
|
|
|
# Option -V for verbose output
|
|
if ( defined($::opt_V) ) {
|
|
$::verbose = 1;
|
|
$::VERBOSE = 1;
|
|
}
|
|
|
|
|
|
if ( $::command eq "rollupdate" ) {
|
|
# process @ARGV
|
|
#while (my $a = shift(@ARGV))
|
|
#{
|
|
# no args for command yet
|
|
#}
|
|
|
|
# process the <stdin> input file
|
|
if ( defined($::stdindata) ) {
|
|
my $rc = readFileInput($::stdindata);
|
|
if ($rc) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not process file input data.\n";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
}
|
|
else {
|
|
# No <stdin> stanza file, print usage
|
|
return 2;
|
|
}
|
|
}
|
|
elsif ( $::command eq "runrollupdate" ) {
|
|
# process @ARGV
|
|
$::scheduler = shift(@ARGV);
|
|
$::datafile = shift(@ARGV);
|
|
$::ll_reservation_id = shift(@ARGV);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 readFileInput
|
|
|
|
Process the command line input piped in from a stanza file.
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
Set %::FILEATTRS
|
|
(i.e.- $::FILEATTRS{attr}=[val])
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub readFileInput {
|
|
my ($filedata) = @_;
|
|
|
|
my @lines = split /\n/, $filedata;
|
|
|
|
my $rf_rc=0;
|
|
my $rf_err= "";
|
|
my $prev_attr = "";
|
|
foreach my $l (@lines) {
|
|
|
|
# skip blank and comment lines
|
|
next if ( $l =~ /^\s*$/ || $l =~ /^\s*#/ );
|
|
|
|
# process a real line
|
|
if ( $l =~ /^\s*(\w+)\s*=\s*(.*)\s*/ ) {
|
|
my $attr = $1;
|
|
my $val = $2;
|
|
my $orig_attr = $attr;
|
|
my $orig_val = $val;
|
|
$attr =~ s/^\s*//; # Remove any leading whitespace
|
|
$attr =~ s/\s*$//; # Remove any trailing whitespace
|
|
$attr =~ tr/A-Z/a-z/; # Convert to lowercase
|
|
$val =~ s/^\s*//;
|
|
$val =~ s/\s*$//;
|
|
|
|
# Convert the following values to lowercase
|
|
if ( ($attr eq 'scheduler') ||
|
|
($attr eq 'updateall') ||
|
|
($attr eq 'update_if_down') ||
|
|
($attr eq 'skipshutdown') ) {
|
|
$val =~ tr/A-Z/a-z/;
|
|
if ( ($attr eq 'scheduler') &&
|
|
($val ne 'loadleveler') ) {
|
|
$rf_err = "Stanza \'$orig_attr = $orig_val\' is not valid. Valid values are \'loadleveler\'.";
|
|
$rf_rc=1;
|
|
my $rsp;
|
|
push @{ $rsp->{data} },$rf_err;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
}
|
|
if ( ($attr eq 'updateall') &&
|
|
(($val ne 'yes') &&
|
|
($val ne 'y') &&
|
|
($val ne 'no') &&
|
|
($val ne 'n') ) ) {
|
|
$rf_err = "Stanza \'$orig_attr = $orig_val\' is not valid. Valid values are \'yes\' or \'no\'.";
|
|
$rf_rc=1;
|
|
my $rsp;
|
|
push @{ $rsp->{data} },$rf_err;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
}
|
|
if ( ($attr eq 'update_if_down') &&
|
|
(($val ne 'yes') &&
|
|
($val ne 'y') &&
|
|
($val ne 'no') &&
|
|
($val ne 'n') &&
|
|
($val ne 'cancel') ) ) {
|
|
$rf_err = "Stanza \'$orig_attr = $orig_val\' is not valid. Valid values are \'yes\', \'no\', or \'cancel\'.";
|
|
$rf_rc=1;
|
|
my $rsp;
|
|
push @{ $rsp->{data} },$rf_err;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
}
|
|
if ( ($attr eq 'skipshutdown') &&
|
|
(($val ne 'yes') &&
|
|
($val ne 'y') &&
|
|
($val ne 'no') &&
|
|
($val ne 'n') ) ) {
|
|
$rf_err = "Stanza \'$orig_attr = $orig_val\' is not valid. Valid values are \'yes\' or \'no\'.";
|
|
$rf_rc=1;
|
|
my $rsp;
|
|
push @{ $rsp->{data} },$rf_err;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
}
|
|
}
|
|
|
|
# Remove surrounding quotes in the following values
|
|
if ( ($attr eq 'bringupappstatus') ) {
|
|
$val =~ s/^['"]//;
|
|
$val =~ s/['"]$//;
|
|
}
|
|
|
|
# Set some required defaults if not specified
|
|
if (($prev_attr eq "prescript") && ($attr ne "prescriptnodes")) {
|
|
push ( @{ $::FILEATTRS{'prescriptnodes'} }, 'ALL_NODES_IN_UPDATEGROUP' );
|
|
}
|
|
if (($prev_attr eq "outofbandcmd") && ($attr ne "outofbandnodes")) {
|
|
push ( @{ $::FILEATTRS{'outofbandnodes'} }, 'ALL_NODES_IN_UPDATEGROUP' );
|
|
}
|
|
if (($prev_attr eq "mutex") && ($attr ne "mutex_count")) {
|
|
push ( @{ $::FILEATTRS{'mutex_count'} }, '1' );
|
|
}
|
|
if (($prev_attr eq "nodegroup_mutex") && ($attr eq "mutex_count")) {
|
|
$attr = "nodegroup_mutex_count";
|
|
}
|
|
if (($prev_attr eq "nodegroup_mutex") && ($attr ne "nodegroup_mutex_count")) {
|
|
push ( @{ $::FILEATTRS{'nodegroup_mutex_count'} }, '1' );
|
|
}
|
|
|
|
# set the value in the hash for this entry
|
|
push( @{ $::FILEATTRS{$attr} }, $val );
|
|
$prev_attr = $attr;
|
|
}
|
|
} # end while - go to next line
|
|
|
|
return $rf_rc;
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 rollupdate
|
|
|
|
Support for the xCAT rollupdate command.
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
sub rollupdate {
|
|
|
|
my $rc = 0;
|
|
my $error = 0;
|
|
|
|
# process the command line
|
|
$rc = &processArgs;
|
|
if ( $rc != 0 ) {
|
|
|
|
# rc: 0 - ok, 1 - return, 2 - help, 3 - error
|
|
if ( $rc != 1 ) {
|
|
&rollupdate_usage;
|
|
}
|
|
return ( $rc - 1 );
|
|
}
|
|
if ($::VERBOSE) {
|
|
unless ( -d $::LOGDIR) {
|
|
unless ( File::Path::mkpath($::LOGDIR) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not create directory $::LOGDIR, logging is disabled.";
|
|
xCAT::MsgUtils->message( "W", $rsp, $::CALLBACK );
|
|
$::VERBOSE = 0;
|
|
$::verbose = 0;
|
|
}
|
|
}
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Running rollupdate command... ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG "\n\n";
|
|
print RULOG localtime()." Running rollupdate command...\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
#
|
|
# Build updategroup nodelists
|
|
#
|
|
my %updategroup;
|
|
|
|
# Check for updateall and required stanzas
|
|
$::updateall=0;
|
|
$::updateall_nodecount=1;
|
|
if ( defined($::FILEATTRS{updateall}[0]) &&
|
|
( ($::FILEATTRS{updateall}[0] eq 'yes') ||
|
|
($::FILEATTRS{updateall}[0] eq 'y' ) ) ) {
|
|
$::updateall=1;
|
|
if ( defined($::FILEATTRS{updateall_nodes}[0])){
|
|
my $ugname = "UPDATEALL".time();
|
|
my $ugval = $::FILEATTRS{updateall_nodes}[0];
|
|
@{ $updategroup{$ugname} } = xCAT::NodeRange::noderange($ugval);
|
|
} else {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: updateall=yes but no updateall_nodes specified. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ( defined($::FILEATTRS{updateall_nodecount}[0]) ) {
|
|
$::updateall_nodecount=$::FILEATTRS{updateall_nodecount}[0];
|
|
} else {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: updateall=yes but no updateall_nodecount specified. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
} else {
|
|
# Standard update (NOT updateall)
|
|
foreach my $ugline ( @{ $::FILEATTRS{'updategroup'} } ) {
|
|
my ( $ugname, $ugval ) = split( /\(/, $ugline );
|
|
$ugval =~ s/\)$//; # remove trailing ')'
|
|
@{ $updategroup{$ugname} } = xCAT::NodeRange::noderange($ugval);
|
|
if ( xCAT::NodeRange::nodesmissed() ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Error processing stanza line: ";
|
|
push @{ $rsp->{data} }, "updategroup=" . $ugline;
|
|
push @{ $rsp->{data} }, "Invalid nodes in noderange: "
|
|
. join( ',', xCAT::NodeRange::nodesmissed() );
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
my $prt_ugn = join(',',@{$updategroup{$ugname}});
|
|
push @{ $rsp->{data} }, "Creating update group $ugname with nodes: ";
|
|
push @{ $rsp->{data} }, $prt_ugn;
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Creating update group $ugname with nodes: \n";
|
|
print RULOG "$prt_ugn\n";
|
|
close (RULOG);
|
|
}
|
|
}
|
|
|
|
foreach my $mgline ( @{ $::FILEATTRS{'mapgroups'} } ) {
|
|
#my @ugnamelist = xCAT::NameRange::namerange( $mgline, 0 );
|
|
my @ugnamelist = xCAT::NodeRange::noderange( $mgline, 0, 1, genericrange=>1 );
|
|
foreach my $ugname (@ugnamelist) {
|
|
@{ $updategroup{$ugname} } = xCAT::NodeRange::noderange($ugname);
|
|
if ( xCAT::NodeRange::nodesmissed() ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Error processing stanza line: ";
|
|
push @{ $rsp->{data} }, "mapgroups=" . $mgline;
|
|
push @{ $rsp->{data} }, "Invalid nodes in group $ugname: "
|
|
. join( ',', xCAT::NodeRange::nodesmissed() );
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
my $prt_ugn = join(',',@{$updategroup{$ugname}});
|
|
push @{ $rsp->{data} }, "Creating update group $ugname with nodes: ";
|
|
push @{ $rsp->{data} }, $prt_ugn;
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Creating update group $ugname with nodes: \n";
|
|
print RULOG "$prt_ugn\n";
|
|
close (RULOG);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
unless (%updategroup) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: No updategroup or mapgroups entries found. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
|
|
|
|
#
|
|
# Build and submit scheduler jobs
|
|
#
|
|
my $scheduler = $::FILEATTRS{'scheduler'}[0];
|
|
if ( ( !$scheduler )
|
|
|| ( $scheduler eq "loadleveler" ) )
|
|
{
|
|
$rc = ll_jobs( \%updategroup );
|
|
}
|
|
else {
|
|
|
|
# TODO: support scheduler plugins here
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Error processing stanza line: ";
|
|
push @{ $rsp->{data} }, "scheduler=" . $::FILEATTRS{'scheduler'}[0];
|
|
push @{ $rsp->{data} }, "Scheduler not supported";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
|
|
return $rc;
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 ll_jobs
|
|
|
|
Build and submit LoadLeveler jobs
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
sub ll_jobs {
|
|
my $updategroup = shift;
|
|
my $rc = 0;
|
|
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Creating LL job command files ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Creating LL job command files \n";
|
|
close (RULOG);
|
|
}
|
|
|
|
# Verify scheduser exists and can run xCAT runrollupdate cmd
|
|
# Get LL userid
|
|
my $lluser = $::FILEATTRS{scheduser}[0];
|
|
unless ( defined($lluser) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: No scheduser entries found. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
my ( $login, $pass, $uid, $gid );
|
|
( $login, $pass, $uid, $gid ) = getpwnam($lluser);
|
|
unless ( defined($uid) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: scheduser userid $lluser not defined in system. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ( &check_policy($lluser,'runrollupdate') ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: scheduser userid $lluser not listed in xCAT policy table for runrollupdate command. Add to policy table and ensure userid has ssh credentials for running xCAT commands. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
|
|
# Translate xCAT names to LL names
|
|
$::XLATED = {};
|
|
if (defined($::FILEATTRS{translatenames}[0])) {
|
|
foreach my $xlate_stanza( @{ $::FILEATTRS{'translatenames'} } ) {
|
|
translate_names($xlate_stanza);
|
|
}
|
|
}
|
|
|
|
|
|
# Create LL floating resources for mutual exclusion support
|
|
# and max_updates
|
|
if (&create_LL_mutex_resources($updategroup,$::updateall) > 0) {
|
|
return 1;
|
|
}
|
|
|
|
#
|
|
# Load job command file template
|
|
#
|
|
my $tmpl_file_name = $::FILEATTRS{'jobtemplate'}[0];
|
|
unless ( defined($tmpl_file_name) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: No jobtemplate entries found. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
|
|
my $TMPL_FILE;
|
|
unless ( open( $TMPL_FILE, "<", $tmpl_file_name ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: jobtemplate file $tmpl_file_name not found. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Reading LL job template file $tmpl_file_name ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Reading LL job template file $tmpl_file_name \n";
|
|
close (RULOG);
|
|
}
|
|
my @lines = <$TMPL_FILE>;
|
|
close $TMPL_FILE;
|
|
|
|
# Query LL for list of machines and their status
|
|
my $cmd = "llstatus -r %n %sta 2>/dev/null";
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Running command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command: $cmd \n";
|
|
close (RULOG);
|
|
}
|
|
my @llstatus = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ( $::RUNCMD_RC != 0 ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not run llstatus command.";
|
|
push @{ $rsp->{data} }, @llstatus;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Could not run llstatus command. \n";
|
|
print RULOG @llstatus;
|
|
close (RULOG);
|
|
return 1;
|
|
}
|
|
my %machines;
|
|
foreach my $machineline (@llstatus) {
|
|
my ( $mlong, $mshort, $mstatus );
|
|
( $mlong, $mstatus ) = split( /\!/, $machineline );
|
|
($mshort) = split( /\./, $mlong );
|
|
$machines{$mlong} = { mname => $mlong, mstatus => $mstatus };
|
|
if ( !( $mlong eq $mshort ) ) {
|
|
$machines{$mshort} = { mname => $mlong, mstatus => $mstatus };
|
|
}
|
|
}
|
|
|
|
#
|
|
# Generate job command file for each updategroup
|
|
#
|
|
|
|
my $lljobs_dir = $::FILEATTRS{jobdir}[0];
|
|
unless ( defined($lljobs_dir) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Error processing stanza input: No jobdir entries found. ";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
unless ( -d $lljobs_dir ) {
|
|
unless ( File::Path::mkpath($lljobs_dir) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not create directory $lljobs_dir";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
unless ( chown( $uid, $gid, $lljobs_dir ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Could not change owner of directory $lljobs_dir to $lluser";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
my $nodestatus = $::FILEATTRS{bringupstatus}[0];
|
|
my $appstatus = $::FILEATTRS{bringupappstatus}[0];
|
|
my $df_statusline = "";
|
|
if ( defined($appstatus) ) {
|
|
$df_statusline = "bringupappstatus=$appstatus\n";
|
|
} elsif ( defined($nodestatus) ) {
|
|
$df_statusline = "bringupstatus=$nodestatus\n";
|
|
} else {
|
|
$df_statusline = "bringupstatus=booted\n";
|
|
}
|
|
|
|
my $run_if_down = "cancel";
|
|
if ( defined($::FILEATTRS{update_if_down}[0]) ) {
|
|
$run_if_down = $::FILEATTRS{update_if_down}[0];
|
|
$run_if_down =~ tr/[A-Z]/[a-z]/;
|
|
if ( $run_if_down eq 'y' ) { $run_if_down = 'yes'; }
|
|
if ( $::updateall && ($run_if_down eq 'yes') ) {
|
|
$run_if_down = 'cancel';
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "update_all=yes, but update_if_down is yes which is not allowed. update_if_down=cancel will be assumed. ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
}
|
|
}
|
|
|
|
my @calldirectly;
|
|
ugloop: foreach my $ugname ( keys %{$updategroup} ) {
|
|
|
|
$::updateall_feature = " ";
|
|
if ($::updateall) { $::updateall_feature = "XCAT_$ugname"; }
|
|
# Build node substitution strings
|
|
my ( $nodelist, $machinelist );
|
|
my $machinecount = 0;
|
|
foreach my $node ( @{ $updategroup->{$ugname} } ) {
|
|
my $xlated_node;
|
|
if ( defined ($::XLATED{$node}) ){
|
|
$xlated_node = $::XLATED{$node};
|
|
} else {
|
|
$xlated_node = $node;
|
|
}
|
|
if ( defined( $machines{$xlated_node} )
|
|
&& ( $machines{$xlated_node}{'mstatus'} eq "1" ) ) {
|
|
$machinelist .= " $machines{$xlated_node}{'mname'}";
|
|
$machinecount++;
|
|
$nodelist .= ",$node";
|
|
} elsif ( $run_if_down eq 'yes' ) {
|
|
if ( defined( $machines{$xlated_node} ) ) {
|
|
# llmkres -D will allow reserving down nodes as long
|
|
# as they are present in the machine list
|
|
$machinelist .= " $machines{$xlated_node}{'mname'}";
|
|
$machinecount++;
|
|
}
|
|
$nodelist .= ",$node";
|
|
} elsif ( $run_if_down eq 'cancel' ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"Node $node is not active in LL and \"update_if_down=cancel\". Update for updategroup $ugname is canceled.";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
++$rc;
|
|
next ugloop;
|
|
}
|
|
}
|
|
if ( ($machinecount == 0) && ($::updateall) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"\"updateall=yes\" and \"update_if_down=no\", but there are no nodes specifed in the updateall noderange that are currently active in LoadLeveler. Update for updategroup $ugname is canceled.";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
++$rc;
|
|
next ugloop;
|
|
}
|
|
|
|
if ( defined($nodelist) ) { $nodelist =~ s/^\,//; }
|
|
# Build updategroup data file
|
|
my @ugdflines;
|
|
push (@ugdflines, "# xCAT Rolling Update data file for update group $ugname \n");
|
|
push (@ugdflines, "\n");
|
|
push (@ugdflines, "updategroup=$ugname\n");
|
|
if ($::updateall){
|
|
push (@ugdflines, "updatefeature=$::updateall_feature\n");
|
|
} else {
|
|
push (@ugdflines, "nodelist=$nodelist\n");
|
|
}
|
|
if (defined($::FILEATTRS{oldfeature}[0])){
|
|
push (@ugdflines, "oldfeature=$::FILEATTRS{oldfeature}[0]\n");
|
|
}
|
|
if (defined($::FILEATTRS{newfeature}[0])){
|
|
push (@ugdflines, "newfeature=$::FILEATTRS{newfeature}[0]\n");
|
|
}
|
|
if (defined($::FILEATTRS{reconfiglist}[0])){
|
|
push (@ugdflines, "reconfiglist=$::FILEATTRS{reconfiglist}[0]\n");
|
|
}
|
|
push (@ugdflines, "\n");
|
|
push (@ugdflines, &get_prescripts($nodelist));
|
|
if (defined($::FILEATTRS{shutdowntimeout}[0])){
|
|
push (@ugdflines, "shutdowntimeout=$::FILEATTRS{shutdowntimeout}[0]\n");
|
|
}
|
|
push (@ugdflines, &get_outofband($nodelist));
|
|
push (@ugdflines, &get_bringuporder($nodelist));
|
|
push (@ugdflines, $df_statusline);
|
|
if (defined($::FILEATTRS{bringuptimeout}[0])){
|
|
push (@ugdflines, "bringuptimeout=$::FILEATTRS{bringuptimeout}[0]\n");
|
|
}
|
|
if (defined($::FILEATTRS{translatenames}[0])){
|
|
foreach my $xlate_stanza( @{ $::FILEATTRS{'translatenames'} } ) {
|
|
push (@ugdflines, "translatenames=$xlate_stanza\n");
|
|
}
|
|
}
|
|
if (defined($::FILEATTRS{skipshutdown}[0])){
|
|
push (@ugdflines, "skipshutdown=$::FILEATTRS{skipshutdown}[0]\n");
|
|
}
|
|
my $ugdf_file = $lljobs_dir . "/rollupdate_" . $ugname . ".data";
|
|
my $UGDFFILE;
|
|
unless ( open( $UGDFFILE, ">$ugdf_file" ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not open file $ugdf_file";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Writing xCAT rolling update data file $ugdf_file ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Writing xCAT rolling update data file $ugdf_file \n";
|
|
close (RULOG);
|
|
}
|
|
print $UGDFFILE @ugdflines;
|
|
close($UGDFFILE);
|
|
chown( $uid, $gid, $ugdf_file );
|
|
|
|
if ($machinecount > 0) {
|
|
my $llhl_file;
|
|
if ( !$::updateall ) {
|
|
# Build LL hostlist file
|
|
$machinelist =~ s/^\s+//;
|
|
my $hllines = $machinelist;
|
|
$hllines =~ s/"//g;
|
|
$hllines =~ s/\s+/\n/g;
|
|
$hllines .= "\n";
|
|
$llhl_file = $lljobs_dir . "/rollupdate_" . $ugname . ".hostlist";
|
|
my $HLFILE;
|
|
unless ( open( $HLFILE, ">$llhl_file" ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not open file $llhl_file";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Writing LL hostlist file $llhl_file ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
}
|
|
print $HLFILE $hllines;
|
|
close($HLFILE);
|
|
chown( $uid, $gid, $llhl_file );
|
|
}
|
|
|
|
# Build reservation callback script
|
|
|
|
my @rcblines;
|
|
my $rcbcmd = $::FILEATTRS{'reservationcallback'}[0];
|
|
if (!defined($rcbcmd)){ $rcbcmd = "$::XCATROOT/bin/runrollupdate"; }
|
|
push (@rcblines, "#!/bin/sh \n");
|
|
push (@rcblines, "# LL Reservation Callback script for xCAT Rolling Update group $ugname \n");
|
|
push (@rcblines, "\n");
|
|
push (@rcblines, "if [ \"\$2\" == \"RESERVATION_ACTIVE\" ] ; then\n");
|
|
my $send_verbose = "";
|
|
if ($::VERBOSE) {$send_verbose="--verbose";}
|
|
push (@rcblines, " $rcbcmd $send_verbose loadleveler $ugdf_file \$1 &\n");
|
|
push (@rcblines, "fi \n");
|
|
push (@rcblines, "\n");
|
|
my $llrcb_file = $lljobs_dir . "/rollupdate_" . $ugname . ".rsvcb";
|
|
my $RCBFILE;
|
|
unless ( open( $RCBFILE, ">$llrcb_file" ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not open file $llrcb_file";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Writing LL reservation callback script $llrcb_file ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Writing LL reservation callback script $llrcb_file \n";
|
|
close (RULOG);
|
|
}
|
|
print $RCBFILE @rcblines;
|
|
close($RCBFILE);
|
|
chown( $uid, $gid, $llrcb_file );
|
|
chmod( 0700, $llrcb_file );
|
|
|
|
# Build output file
|
|
my $mutex_string = &get_mutex($ugname);
|
|
my $lastcount = 0;
|
|
my $llcount = $machinecount;
|
|
if (($::updateall) &&
|
|
($machinecount gt $::updateall_nodecount)) {
|
|
$lastcount = $machinecount % $::updateall_nodecount;
|
|
$llcount = $::updateall_nodecount;
|
|
}
|
|
my @jclines;
|
|
my @jclines2;
|
|
foreach my $line (@lines) {
|
|
my $jcline = $line;
|
|
my $jcline2;
|
|
$jcline =~ s/\[\[NODESET\]\]/$ugname/;
|
|
$jcline =~ s/\[\[JOBDIR\]\]/$lljobs_dir/;
|
|
if (defined($nodelist)){
|
|
$jcline =~ s/\[\[XNODELIST\]\]/$nodelist/;
|
|
} else {
|
|
$jcline =~ s/\[\[XNODELIST\]\]//;
|
|
}
|
|
if (defined($llhl_file)){
|
|
$jcline =~ s/\[\[LLHOSTFILE\]\]/$llhl_file/;
|
|
} else {
|
|
$jcline =~ s/\[\[LLHOSTFILE\]\]//;
|
|
}
|
|
if (defined($::FILEATTRS{oldfeature}[0])){
|
|
$jcline =~ s/\[\[OLDFEATURE\]\]/$::FILEATTRS{oldfeature}[0]/;
|
|
} else {
|
|
$jcline =~ s/\[\[OLDFEATURE\]\]//;
|
|
}
|
|
$jcline =~ s/\[\[UPDATEALLFEATURE\]\]/$::updateall_feature/;
|
|
$jcline =~ s/\[\[MUTEXRESOURCES\]\]/$mutex_string/;
|
|
# LL is VERY picky about extra blanks in Feature string
|
|
if ( $jcline =~ /Feature/ ) {
|
|
$jcline =~ s/\"\s+/\"/g;
|
|
$jcline =~ s/\s+\"/\"/g;
|
|
$jcline =~ s/\=\"/\= \"/g;
|
|
}
|
|
if ($lastcount) {
|
|
$jcline2 = $jcline;
|
|
$jcline2 =~ s/\[\[LLCOUNT\]\]/$lastcount/;
|
|
push( @jclines2, $jcline2 );
|
|
}
|
|
if ( $jcline =~ /\[\[LLCOUNT\]\]/ ) {
|
|
$jcline =~ s/\[\[LLCOUNT\]\]/$llcount/;
|
|
}
|
|
push( @jclines, $jcline );
|
|
}
|
|
my $lljob_file = $lljobs_dir . "/rollupdate_" . $ugname . ".cmd";
|
|
my $JOBFILE;
|
|
unless ( open( $JOBFILE, ">$lljob_file" ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not open file $lljob_file";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Writing LL job command file $lljob_file ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Writing LL job command file $lljob_file \n";
|
|
close (RULOG);
|
|
}
|
|
print $JOBFILE @jclines;
|
|
close($JOBFILE);
|
|
chown( $uid, $gid, $lljob_file );
|
|
my $lljob_file2 = $lljobs_dir . "/rollupdate_LAST_" . $ugname . ".cmd";
|
|
if ($lastcount) {
|
|
my $JOBFILE2;
|
|
unless ( open( $JOBFILE2, ">$lljob_file2" ) ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not open file $lljob_file2";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Writing LL job command file $lljob_file2 ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Writing LL job command file $lljob_file2 \n";
|
|
close (RULOG);
|
|
}
|
|
print $JOBFILE2 @jclines2;
|
|
close($JOBFILE2);
|
|
chown( $uid, $gid, $lljob_file2 );
|
|
}
|
|
|
|
if ($::updateall) {
|
|
&set_LL_feature($machinelist,$::updateall_feature);
|
|
|
|
}
|
|
|
|
# Need to change status before actually submitting LL jobs
|
|
# If LL jobs happen to run right away, the update code checking
|
|
# for the status may run before we've had a chance to actually update it
|
|
my $nltab = xCAT::Table->new('nodelist');
|
|
my @nodes = split( /\,/, $nodelist );
|
|
xCAT::TableUtils->setAppStatus(\@nodes,"RollingUpdate","update_job_submitted");
|
|
# Submit LL reservation
|
|
my $downnodes = "-D";
|
|
if ($::updateall){$downnodes = " ";}
|
|
my $cmd = qq~su - $lluser "-c llmkres -x -d $::FILEATTRS{'reservationduration'}[0] -f $lljob_file -p $llrcb_file $downnodes "~;
|
|
my $cmd2 = qq~su - $lluser "-c llmkres -x -d $::FILEATTRS{'reservationduration'}[0] -f $lljob_file2 -p $llrcb_file $downnodes "~;
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Running command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command: $cmd \n";
|
|
close (RULOG);
|
|
}
|
|
my @llsubmit;
|
|
if ($::TEST) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "In TEST mode. Will NOT run command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
$::RUNCMD_RC = 0;
|
|
} else {
|
|
my $submit_count = 1;
|
|
if ($::updateall){
|
|
$submit_count = int($machinecount / $::updateall_nodecount);
|
|
if ($submit_count == 0) {$submit_count = 1;}
|
|
}
|
|
for (1..$submit_count) {
|
|
@llsubmit = xCAT::Utils->runcmd( "$cmd", 0 );
|
|
if ( $::RUNCMD_RC != 0 ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not run llmkres command.";
|
|
push @{ $rsp->{data} }, @llsubmit;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
if ( $::VERBOSE ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, @llsubmit;
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG @llsubmit;
|
|
close (RULOG);
|
|
}
|
|
}
|
|
if ($lastcount) {
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Running command: $cmd2 ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command: $cmd2 \n";
|
|
close (RULOG);
|
|
}
|
|
@llsubmit = xCAT::Utils->runcmd( "$cmd2", 0 );
|
|
if ( $::RUNCMD_RC != 0 ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Could not run llmkres command.";
|
|
push @{ $rsp->{data} }, @llsubmit;
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG @llsubmit;
|
|
close (RULOG);
|
|
return 1;
|
|
}
|
|
if ( $::VERBOSE ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, @llsubmit;
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG @llsubmit;
|
|
close (RULOG);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elsif ( defined($nodelist) ) {
|
|
|
|
# No nodes in LL to submit job to -- not able to schedule.
|
|
# Call xCAT directly for all other nodes.
|
|
# TODO - this will serialize updating the updategroups
|
|
# is this okay, or do we want forked child processes?
|
|
push @calldirectly, $ugname;
|
|
}
|
|
} # end ugloop
|
|
|
|
if ( scalar(@calldirectly) > 0 ) {
|
|
my @children;
|
|
foreach my $ugname (@calldirectly) {
|
|
my $nodelist = join( ',', @{ $updategroup->{$ugname} } );
|
|
my $ugdf = $lljobs_dir . "/rollupdate_" . $ugname . ".data";
|
|
my $rsp;
|
|
push @{ $rsp->{data} },
|
|
"No active LL nodes in update group $ugname";
|
|
push @{ $rsp->{data} },
|
|
"These nodes will be updated now. This will take a few minutes...";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." No active LL nodes in update group $ugname. These nodes will be updated now.\n";
|
|
close (RULOG);
|
|
}
|
|
my $nltab = xCAT::Table->new('nodelist');
|
|
my @nodes = split( /\,/, $nodelist );
|
|
xCAT::TableUtils->setAppStatus(\@nodes,"RollingUpdate","update_job_submitted");
|
|
my $childpid = runrollupdate( { command => ['runrollupdate'],
|
|
arg => [ 'internal','loadleveler', $ugdf ]
|
|
});
|
|
if (defined($childpid) && ($childpid != 0)) {
|
|
push (@children, $childpid);
|
|
}
|
|
}
|
|
# wait until all the children are finished before returning
|
|
foreach my $child (@children) {
|
|
if ($::VERBOSE) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Waiting for child PID $child to complete ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Waiting for child PID $child to complete \n";
|
|
close (RULOG);
|
|
}
|
|
waitpid($child,0);
|
|
}
|
|
}
|
|
|
|
return $rc;
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 check_policy
|
|
|
|
Check the policy table to see if userid is authorized to run command
|
|
|
|
Arguments: userid, command
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub check_policy {
|
|
my $userid = shift;
|
|
my $xcatcmd = shift;
|
|
|
|
my $policytable = xCAT::Table->new('policy');
|
|
unless ($policytable) {
|
|
return 1;
|
|
}
|
|
|
|
my $policies = $policytable->getAllEntries;
|
|
$policytable->close;
|
|
foreach my $rule (@$policies) {
|
|
if ( $rule->{name} &&
|
|
(($rule->{name} eq "*") || ($rule->{name} eq $userid)) ) {
|
|
if ( $rule->{commands} ) {
|
|
if (($rule->{commands} eq "") || ($rule->{commands} eq "*") || ($rule->{commands} =~ /$xcatcmd/) ){
|
|
return 0; # match found
|
|
}
|
|
} else {
|
|
return 0; # default match if commands is unset
|
|
}
|
|
}
|
|
}
|
|
return 1; # no match found
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 translate_names
|
|
|
|
Translate xCAT node names to scheduler names as requested by the user
|
|
|
|
Arguments: $instructions - translation instructions of the form:
|
|
<xcat_noderange>:/<pattern>/<replacement>/
|
|
OR
|
|
<xcat_noderange>:|<pattern>|<replacement>|
|
|
Returns:
|
|
Globals:
|
|
hash: $::XLATED{$node}=$xlated_name
|
|
AND $::XLATED{$xlated_name}=$node
|
|
to allow easy lookup in either direction
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# This is a utility function to create a number out of a string, useful for things like round robin algorithms on unnumbered nodes
|
|
sub mknum {
|
|
my $string = shift;
|
|
my $number=0;
|
|
foreach (unpack("C*",$string)) { #do big endian, then it would make 'fred' and 'free' be one number apart
|
|
$number += $_;
|
|
}
|
|
return $number;
|
|
}
|
|
$evalcpt->share('&mknum');
|
|
$evalcpt->permit('require');
|
|
|
|
sub translate_names{
|
|
my $instructions = shift;
|
|
|
|
my ($nr,$regexps) = split( /\:/, $instructions );
|
|
my @xCATnodes = xCAT::NodeRange::noderange($nr);
|
|
|
|
foreach my $xCATnode (@xCATnodes) {
|
|
my $xlated_node = $xCATnode;
|
|
my $datum = $regexps;
|
|
# The following is based on code copied from Table::getNodeAttribs
|
|
if ($datum =~ /^\/[^\/]*\/[^\/]*\/$/)
|
|
{
|
|
my $exp = substr($datum, 1);
|
|
chop $exp;
|
|
my @parts = split('/', $exp, 2);
|
|
$xlated_node =~ s/$parts[0]/$parts[1]/;
|
|
$datum = $xlated_node;
|
|
}
|
|
elsif ($datum =~ /^\|.*\|.*\|$/)
|
|
{
|
|
#Perform arithmetic and only arithmetic operations in bracketed issues on the right.
|
|
#Tricky part: don't allow potentially dangerous code, only eval if
|
|
#to-be-evaled expression is only made up of ()\d+-/%$
|
|
#Futher paranoia? use Safe module to make sure I'm good
|
|
my $exp = substr($datum, 1);
|
|
chop $exp;
|
|
my @parts = split('\|', $exp, 2);
|
|
my $curr;
|
|
my $next;
|
|
my $prev;
|
|
my $retval = $parts[1];
|
|
($curr, $next, $prev) =
|
|
extract_bracketed($retval, '()', qr/[^()]*/);
|
|
|
|
unless($curr) { #If there were no paramaters to save, treat this one like a plain regex
|
|
undef $@; #extract_bracketed would have set $@ if it didn't return, undef $@
|
|
$retval = $xlated_node;
|
|
$retval =~ s/$parts[0]/$parts[1]/;
|
|
$datum = $retval;
|
|
unless ($datum =~ /^$/) { # ignore blank translations
|
|
$xlated_node=$datum;
|
|
}
|
|
# next; #skip the redundancy that follows otherwise
|
|
}
|
|
while ($curr)
|
|
{
|
|
|
|
#my $next = $comps[0];
|
|
my $value = $xlated_node;
|
|
$value =~ s/$parts[0]/$curr/;
|
|
# $value = $evalcpt->reval('use integer;'.$value);
|
|
$value = $evalcpt->reval($value);
|
|
$retval = $prev . $value . $next;
|
|
#use text::balanced extract_bracketed to parse each atom, make sure nothing but arith operators, parens, and numbers are in it to guard against code execution
|
|
($curr, $next, $prev) =
|
|
extract_bracketed($retval, '()', qr/[^()]*/);
|
|
}
|
|
undef $@;
|
|
#At this point, $retval is the expression after being arithmetically contemplated, a generated regex, and therefore
|
|
#must be applied in total
|
|
my $answval = $xlated_node;
|
|
$answval =~ s/$parts[0]/$retval/;
|
|
$datum = $answval; #$retval;
|
|
}
|
|
unless ($datum =~ /^$/) {
|
|
$::XLATED{$xCATnode}=$datum;
|
|
$::XLATED{$datum}=$xCATnode;
|
|
}
|
|
|
|
}
|
|
# print Dumper($::XLATED);
|
|
return ;
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 set_LL_feature
|
|
|
|
Sets the specified feature for the list of LL machines
|
|
|
|
Arguments: $machinelist - blank delimited list of LL machines
|
|
$feature - feature value to set
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub set_LL_feature {
|
|
my $machinelist = shift;
|
|
my $feature = shift;
|
|
|
|
# Query current feature
|
|
my $cmd = "llconfig -h $machinelist -d FEATURE";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
my @llcfgoutput = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
my %set_features;
|
|
my %had_cfg;
|
|
foreach my $llcfgout (@llcfgoutput) {
|
|
my @llfeatures;
|
|
my $haveit = 0;
|
|
if ( $llcfgout =~ /:FEATURE =/ ) {
|
|
my ($stuff,$curfeature_string) = split(/=/,$llcfgout);
|
|
my $newfeature_string = "";
|
|
my ($machine,$morestuff) = split(/:/,$stuff);
|
|
@llfeatures = split(/\s+/,$curfeature_string);
|
|
foreach my $f (@llfeatures) {
|
|
if ($f =~ /XCAT_UPDATEALL\d*/) {
|
|
$f = "";
|
|
} else {
|
|
$newfeature_string .= " $f";
|
|
}
|
|
if ($f eq $feature){
|
|
$haveit = 1;
|
|
}
|
|
}
|
|
if ( !$haveit) {
|
|
$newfeature_string .= " $feature";
|
|
}
|
|
$set_features{$newfeature_string} .= " $machine";
|
|
$had_cfg{$machine} = 1;
|
|
}
|
|
}
|
|
foreach my $m (split(/\s+/,$machinelist)){
|
|
if (! $had_cfg{$m} ){
|
|
$set_features{$feature} .= " $m";
|
|
}
|
|
}
|
|
|
|
# Change in LL database
|
|
foreach my $sf (keys %set_features) {
|
|
if ($set_features{$sf} !~ /^\s*$/){
|
|
$cmd = "llconfig -N -h $set_features{$sf} -c FEATURE=\"$sf\"";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "In TEST mode. Will NOT run command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
$::RUNCMD_RC = 0;
|
|
} else {
|
|
xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
llreconfig();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 get_prescripts
|
|
|
|
Generate the prescripts for this nodelist
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub get_prescripts {
|
|
my $nodelist = shift;
|
|
my @nl = split(/,/, $nodelist);
|
|
|
|
my @datalines;
|
|
my $psindex=0;
|
|
foreach my $psl ( @{ $::FILEATTRS{'prescript'} } ) {
|
|
my $psline = $psl;
|
|
if ($::updateall) {
|
|
push (@datalines, "prescript=".$psline."\n");
|
|
$psindex++;
|
|
next;
|
|
}
|
|
my $psnoderange = $::FILEATTRS{'prescriptnodes'}[$psindex];
|
|
my $executenodelist = "";
|
|
if ($psnoderange eq "ALL_NODES_IN_UPDATEGROUP") {
|
|
$executenodelist = $nodelist;
|
|
} else {
|
|
my @psns = xCAT::NodeRange::noderange($psnoderange);
|
|
unless (@psns) { $psindex++; next; }
|
|
my @executenodes;
|
|
foreach my $node (@nl) {
|
|
push (@executenodes, grep (/^$node$/,@psns));
|
|
}
|
|
$executenodelist = join(',',@executenodes);
|
|
}
|
|
if ($executenodelist ne "") {
|
|
$psline =~ s/\$NODELIST/$executenodelist/g;
|
|
push (@datalines, "prescript=".$psline."\n");
|
|
}
|
|
$psindex++;
|
|
}
|
|
return @datalines;
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 get_outofband
|
|
|
|
Generate the out of band scripts for this nodelist
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub get_outofband {
|
|
my $nodelist = shift;
|
|
my @nl = split(/,/, $nodelist);
|
|
|
|
my @datalines;
|
|
my $obindex=0;
|
|
foreach my $obl ( @{ $::FILEATTRS{'outofbandcmd'} } ) {
|
|
my $obline = $obl;
|
|
if ($::updateall) {
|
|
push (@datalines, "outofbandcmd=".$obline."\n");
|
|
$obindex++;
|
|
next;
|
|
}
|
|
my $obnoderange = $::FILEATTRS{'outofbandnodes'}[$obindex];
|
|
my $executenodelist = "";
|
|
if ($obnoderange eq "ALL_NODES_IN_UPDATEGROUP") {
|
|
$executenodelist = $nodelist;
|
|
} else {
|
|
my @obns = xCAT::NodeRange::noderange($obnoderange);
|
|
unless (@obns) { $obindex++; next; }
|
|
my @executenodes;
|
|
foreach my $node (@nl) {
|
|
push (@executenodes, grep (/^$node$/,@obns));
|
|
}
|
|
$executenodelist = join(',',@executenodes);
|
|
}
|
|
if ($executenodelist ne "") {
|
|
$obline =~ s/\$NODELIST/$executenodelist/g;
|
|
push (@datalines, "outofbandcmd=".$obline."\n");
|
|
}
|
|
$obindex++;
|
|
}
|
|
return @datalines;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 get_bringuporder
|
|
|
|
Generate the bringup order for this nodelist
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub get_bringuporder {
|
|
my $nodelist = shift;
|
|
my @nl = split(/,/, $nodelist);
|
|
|
|
my @datalines;
|
|
if ($::updateall) {
|
|
$datalines[0]="\n";
|
|
return @datalines;
|
|
}
|
|
foreach my $buline ( @{ $::FILEATTRS{'bringuporder'} } ) {
|
|
my @buns = xCAT::NodeRange::noderange($buline);
|
|
unless (@buns) { next; }
|
|
my @bringupnodes;
|
|
foreach my $node (@nl) {
|
|
if (defined($node)) {
|
|
(my $found) = grep (/^$node$/,@buns);
|
|
if ($found) {
|
|
push (@bringupnodes, $found);
|
|
undef $node; # don't try to use this node again
|
|
}
|
|
}
|
|
}
|
|
my $bringupnodelist = join(',',@bringupnodes);
|
|
if ($bringupnodelist ne "") {
|
|
push (@datalines, "bringuporder=".$bringupnodelist."\n");
|
|
}
|
|
}
|
|
return @datalines;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 get_mutex
|
|
|
|
Generate the list of LL mutual exclusion resources for this
|
|
update group
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub get_mutex {
|
|
my $ugname = shift;
|
|
|
|
my $mutex_string = "";
|
|
|
|
my $max_updates = $::FILEATTRS{'maxupdates'}[0];
|
|
if ( defined($max_updates) && ($max_updates ne 'all') ) {
|
|
$mutex_string .= "XCATROLLINGUPDATE_MAXUPDATES(1) ";
|
|
}
|
|
|
|
if ($::updateall){
|
|
return $mutex_string;
|
|
}
|
|
|
|
my $num_mutexes = scalar @::MUTEX;
|
|
if ( $num_mutexes > 0 ) {
|
|
foreach my $row (0..($num_mutexes-1)) {
|
|
foreach my $ugi (0..(@{$::MUTEX[$row]} - 1)) {
|
|
if ( defined($::MUTEX[$row][$ugi]) && ($ugname eq $::MUTEX[$row][$ugi]) ) {
|
|
$mutex_string .= "XCATROLLINGUPDATE_MUTEX".$row."(1) ";
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $mutex_string;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 create_LL_mutex_resources
|
|
|
|
Create all required LL mutex resources
|
|
|
|
Arguments:
|
|
updategroup
|
|
maxupdates_only:
|
|
1 - only create MAXUPDATES resources (for updateall)
|
|
0 - create MAXUPDATES and all MUTEX resources
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments: Reads the mutex entries in $::FILEATTRS and
|
|
sets the global array $::MUTEX
|
|
which is a multi-dimensional array:
|
|
Each row is an array of mutually exclusive update
|
|
group names
|
|
A LL Floating Resource will be created for each
|
|
row of the array.
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub create_LL_mutex_resources {
|
|
|
|
my $updategroup=shift;
|
|
my $maxupdates_only=shift;
|
|
|
|
$::LL_MUTEX_RESOURCES_CREATED = 0;
|
|
my $mxindex=0;
|
|
my $fileattrs_index=0;
|
|
if (!$maxupdates_only) {
|
|
foreach my $mxline ( @{ $::FILEATTRS{'mutex'} } ) {
|
|
my $mx_count = $::FILEATTRS{'mutex_count'}[$fileattrs_index];
|
|
my @mxparts = split(/,/,$mxline);
|
|
if ( scalar @mxparts < 2 ) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "Error processing stanza line: ";
|
|
push @{ $rsp->{data} }, "mutex=" . $mxline;
|
|
push @{ $rsp->{data} }, "Value must contain at least 2 update groups";
|
|
xCAT::MsgUtils->message( "E", $rsp, $::CALLBACK );
|
|
return 1;
|
|
}
|
|
|
|
my $mxpi = 0;
|
|
my $mxindexmax = $mxindex;
|
|
my @ugnames;
|
|
foreach my $mxpart ( @mxparts ) {
|
|
my $mxindex2 = $mxindex;
|
|
#my @ugnamelist = xCAT::NameRange::namerange( $mxpart, 0 );
|
|
my @ugnamelist = xCAT::NodeRange::noderange( $mxpart, 0, 1, genericrange=>1 );
|
|
foreach my $ugname (@ugnamelist) {
|
|
$::MUTEX[$mxindex2][$mxpi] = $ugname;
|
|
$mxindex2++;
|
|
}
|
|
$mxindexmax = ($mxindex2 > $mxindexmax) ? $mxindex2 : $mxindexmax;
|
|
$mxpi++;
|
|
}
|
|
my $mxc;
|
|
for ($mxc=$mxindex; $mxc < $mxindexmax; $mxc++) {
|
|
$::MUTEX_COUNT[$mxc] = $mx_count;
|
|
}
|
|
$mxindex = $mxindexmax;
|
|
$fileattrs_index++;
|
|
}
|
|
|
|
# If nodegroup_mutex entries are specified, we need to use the
|
|
# list of all the nodes in each updategroup for this entire run.
|
|
# Then we need to get a list of all the nodes in the specified
|
|
# nodegroup and look for any intersections to create mutexes.
|
|
$fileattrs_index=0;
|
|
foreach my $mxnodegrp_range ( @{ $::FILEATTRS{'nodegroup_mutex'} } ) {
|
|
my $mx_count = $::FILEATTRS{'nodegroup_mutex_count'}[$fileattrs_index];
|
|
|
|
#foreach my $mxnodegroup ( xCAT::NameRange::namerange( $mxnodegrp_range, 0 ) ) {
|
|
foreach my $mxnodegroup ( xCAT::NodeRange::noderange( $mxnodegrp_range, 0, 1, genericrange=>1 ) ) {
|
|
my $mxpi = 0;
|
|
mxnode_loop: foreach my $mxnode ( xCAT::NodeRange::noderange($mxnodegroup) ) {
|
|
foreach my $ugname ( keys %{$updategroup} ) {
|
|
foreach my $node ( @{ $updategroup->{$ugname} } ) {
|
|
if ($mxnode eq $node) {
|
|
# found a match, add updategroup to this mutex if we
|
|
# don't already have it listed
|
|
my $chk = 0;
|
|
while ( $chk < $mxpi ){
|
|
if ($::MUTEX[$mxindex][$chk] eq $ugname) {
|
|
# already have this one, skip to next
|
|
next mxnode_loop;
|
|
}
|
|
$chk++;
|
|
}
|
|
$::MUTEX[$mxindex][$mxpi] = $ugname;
|
|
$mxpi++;
|
|
next mxnode_loop;
|
|
} # end if found match
|
|
}
|
|
}
|
|
} # end mxnode_loop
|
|
if ($mxpi == 1) {
|
|
# only one updategroup in this mutex, not valid -- ignore it
|
|
delete $::MUTEX[$mxindex];
|
|
} elsif ( $mxpi > 1 ) {
|
|
$::MUTEX_COUNT[$mxindex] = $mx_count;
|
|
$mxindex++;
|
|
}
|
|
}
|
|
$fileattrs_index++;
|
|
}
|
|
}
|
|
|
|
|
|
# Build the actual FLOATING_RESOURCES and SCHEDULE_BY_RESOURCES
|
|
# strings to write into the LL database
|
|
my $resource_string = "";
|
|
my $max_updates = $::FILEATTRS{'maxupdates'}[0];
|
|
if ( ! defined($max_updates) || ($max_updates eq 'all') ) {
|
|
$max_updates = 0;
|
|
} else {
|
|
$resource_string .= "XCATROLLINGUPDATE_MAXUPDATES($max_updates) ";
|
|
}
|
|
|
|
if (!$maxupdates_only) {
|
|
my $num_mutexes = scalar @::MUTEX;
|
|
if ( $num_mutexes > 0 ) {
|
|
foreach my $row (0..($num_mutexes-1)) {
|
|
$resource_string .= "XCATROLLINGUPDATE_MUTEX".$row."($::MUTEX_COUNT[$row]) ";
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $resource_string ) {
|
|
my $cfg_change = 0;
|
|
my $cmd = "llconfig -d FLOATING_RESOURCES SCHEDULE_BY_RESOURCES CENTRAL_MANAGER_LIST RESOURCE_MGR_LIST";
|
|
my @llcfg_d = xCAT::Utils->runcmd( $cmd, 0 );
|
|
my $curSCHED = "";
|
|
my $curFLOAT = "";
|
|
foreach my $cfgo (@llcfg_d) {
|
|
chomp $cfgo;
|
|
my($llattr,$llval) = split (/ = /,$cfgo);
|
|
if ( $llattr =~ /SCHEDULE_BY_RESOURCES/ ) {
|
|
$curSCHED = $llval; }
|
|
if ( $llattr =~ /FLOATING_RESOURCES/ ) {
|
|
$curFLOAT = $llval; }
|
|
}
|
|
my $origSCHED = $curSCHED;
|
|
my $origFLOAT = $curFLOAT;
|
|
$cmd = "llconfig -N -c ";
|
|
$curFLOAT =~ s/XCATROLLINGUPDATE_MUTEX(\d)*\((\d)*\)//g;
|
|
$curFLOAT =~ s/XCATROLLINGUPDATE_MAXUPDATES(\d)*\((\d)*\)//g;
|
|
$curFLOAT .= " $resource_string";
|
|
$curFLOAT =~ s/\s+/ /g; $curFLOAT =~ s/^\s//g; $curFLOAT =~ s/\s$//g;
|
|
$origFLOAT =~ s/\s+/ /g; $origFLOAT =~ s/^\s//g; $origFLOAT =~ s/\s$//g;
|
|
if ( $curFLOAT ne $origFLOAT ) {
|
|
$cmd .= "FLOATING_RESOURCES=\"$curFLOAT\" ";
|
|
$cfg_change = 1;
|
|
}
|
|
|
|
$resource_string =~ s/\((\d)*\)//g;
|
|
$curSCHED =~ s/XCATROLLINGUPDATE_MUTEX(\d)*//g;
|
|
$curSCHED =~ s/XCATROLLINGUPDATE_MAXUPDATES(\d)*//g;
|
|
$curSCHED .= " $resource_string";
|
|
$curSCHED =~ s/\s+/ /g; $curSCHED =~ s/^\s//g; $curSCHED =~ s/\s$//g;
|
|
$origSCHED =~ s/\s+/ /g; $origSCHED =~ s/^\s//g; $origSCHED =~ s/\s$//g;
|
|
if ( $curSCHED ne $origSCHED ) {
|
|
$cmd .= "SCHEDULE_BY_RESOURCES=\"$curSCHED\" ";
|
|
$cfg_change = 1;
|
|
}
|
|
if ($cfg_change) {
|
|
my @llcfg_c;
|
|
if ($::TEST) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "In TEST mode. Will NOT run command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
$::RUNCMD_RC = 0;
|
|
} else {
|
|
@llcfg_c = xCAT::Utils->runcmd( $cmd, 0 );
|
|
}
|
|
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
llreconfig();
|
|
}
|
|
}
|
|
|
|
$::LL_MUTEX_RESOURCES_CREATED = 1;
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 runrollupdate
|
|
|
|
Reboot updategroup in response to request from scheduler job
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
|
|
Error:
|
|
|
|
Example:
|
|
|
|
Comments:
|
|
Note that since this command only gets called from the daemon
|
|
through a port request from a node, there is no active callback
|
|
to return messages to. Log only critical errors to the system log.
|
|
|
|
This subroutine will fork a child process to do the actual
|
|
work of shutting down and rebooting nodes. The parent will return
|
|
immediately to the caller so that other reboot requests can be
|
|
handled in parallel. It is the caller's responsibility to ensure
|
|
the parent process does not go away and take these children down
|
|
with it. (Not a problem when called from xcatd, since that is
|
|
"long-running".)
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
sub runrollupdate {
|
|
|
|
my $reboot_request = shift;
|
|
if ( ! $reboot_request->{arg} ) {
|
|
&runrollupdate_usage;
|
|
return;
|
|
}
|
|
|
|
my @reboot_args = @{$reboot_request->{arg}};
|
|
my $internal = 0;
|
|
if ( $reboot_args[0] eq "internal" ) { $internal = 1; }
|
|
|
|
my $rc = 0;
|
|
my $error = 0;
|
|
|
|
# process the command line
|
|
if ( $internal ) {
|
|
$::scheduler = $reboot_args[1];
|
|
$::datafile = $reboot_args[2];
|
|
$::ll_reservation_id = "";
|
|
} else {
|
|
$rc = &processArgs;
|
|
if ( $rc != 0 ) {
|
|
|
|
# rc: 0 - ok, 1 - return, 2 - help, 3 - error
|
|
if ( $rc != 1 ) {
|
|
&runrollupdate_usage;
|
|
}
|
|
return ( $rc - 1 );
|
|
}
|
|
}
|
|
$::scheduler =~ tr/[A-Z]/[a-z]/;
|
|
|
|
|
|
if ($::VERBOSE) {
|
|
unless (-d $::LOGDIR) { File::Path::mkpath($::LOGDIR); }
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()."runrollupdate request for $::scheduler $::datafile $::ll_reservation_id \n";
|
|
close (RULOG);
|
|
}
|
|
|
|
my $childpid = xCAT::Utils->xfork();
|
|
unless (defined $childpid) { die "Fork failed" };
|
|
if ($childpid != 0) {
|
|
# This is the parent process, just return and let the child do all
|
|
# the work.
|
|
return $childpid;
|
|
}
|
|
|
|
# This is now the child process
|
|
|
|
# Load the datafile
|
|
&readDataFile($::datafile);
|
|
# set some defaults
|
|
$::ug_name = $::DATAATTRS{updategroup}[0];
|
|
my ($statusattr,$statusval,$statustimeout);
|
|
if (defined($::DATAATTRS{bringupappstatus}[0])) {
|
|
$statusattr = "appstatus";
|
|
$statusval = $::DATAATTRS{bringupappstatus}[0];
|
|
} elsif (defined($::DATAATTRS{bringupstatus}[0])) {
|
|
$statusattr="status";
|
|
$statusval = $::DATAATTRS{bringupstatus}[0];
|
|
} else {
|
|
$statusattr="status";
|
|
$statusval = "booted";
|
|
}
|
|
if (defined($::DATAATTRS{bringuptimeout}[0])) {
|
|
$statustimeout = $::DATAATTRS{bringuptimeout}[0];
|
|
} else {
|
|
$statustimeout = 10;
|
|
}
|
|
my $skipshutdown = 0;
|
|
if ((defined($::DATAATTRS{skipshutdown}[0])) &&
|
|
( ($::DATAATTRS{skipshutdown}[0] eq "yes") ||
|
|
($::DATAATTRS{skipshutdown}[0] eq "y") ||
|
|
($::DATAATTRS{skipshutdown}[0] eq "1") ) ) {
|
|
$skipshutdown = 1;
|
|
}
|
|
$::XLATED = {};
|
|
if (defined($::DATAATTRS{translatenames}[0])) {
|
|
foreach my $xlate_stanza( @{ $::DATAATTRS{'translatenames'} } ) {
|
|
translate_names($xlate_stanza);
|
|
}
|
|
}
|
|
|
|
# make sure nodes are in correct state
|
|
my $hostlist = &get_hostlist();
|
|
if (! $hostlist ) {
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()."$::ug_name: Cannot determine nodelist for this request. \n";
|
|
close (RULOG);
|
|
}
|
|
my $rsp;
|
|
xCAT::MsgUtils->message( "S",
|
|
"rollupdate failure: $::ug_name: Cannot determine nodelist for this request. ");
|
|
exit(1);
|
|
}
|
|
|
|
my $nltab = xCAT::Table->new('nodelist');
|
|
my @nodes = split( /\,/, $hostlist );
|
|
my $appstatus=xCAT::TableUtils->getAppStatus(\@nodes,"RollingUpdate");
|
|
foreach my $node (@nodes) {
|
|
unless ( defined($appstatus->{$node})
|
|
&& ( $appstatus->{$node} eq "update_job_submitted" ) )
|
|
{
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Node $node appstatus not in valid state for rolling update\n";
|
|
print RULOG "The following nodelist will not be processed:\n $hostlist \n";
|
|
close (RULOG);
|
|
}
|
|
my $rsp;
|
|
xCAT::MsgUtils->message(
|
|
"S",
|
|
"ROLLUPDATE failure: $::ug_name: Node $node appstatus not in valid state for rolling update "
|
|
);
|
|
if ($::ll_reservation_id){
|
|
my @remove_res;
|
|
$remove_res[0]='CANCEL_DUE_TO_ERROR';
|
|
&remove_LL_reservations(\@remove_res);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
# Run prescripts for this update group
|
|
xCAT::TableUtils->setAppStatus(\@nodes,"RollingUpdate","running_prescripts");
|
|
foreach my $psline ( @{ $::DATAATTRS{'prescript'} } ) {
|
|
$psline =~ s/\$NODELIST/$hostlist/g;
|
|
# Run the command
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running prescript \'$psline\'\n";
|
|
close (RULOG);
|
|
}
|
|
my @psoutput;
|
|
if ($::TEST) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run prescript \'$psline\'\n";
|
|
close (RULOG);
|
|
} else {
|
|
@psoutput = xCAT::Utils->runcmd( $psline, 0 );
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Prescript output:\n";
|
|
foreach my $psoline (@psoutput) {
|
|
print RULOG $psoline."\n";
|
|
}
|
|
close (RULOG);
|
|
}
|
|
}
|
|
|
|
|
|
# Shutdown the nodes
|
|
if ( ! $skipshutdown ) {
|
|
xCAT::TableUtils->setAppStatus(\@nodes,"RollingUpdate","shutting_down");
|
|
my $shutdown_cmd;
|
|
if (xCAT::Utils->isAIX()) { $shutdown_cmd = "shutdown -F &"; }
|
|
else { $shutdown_cmd = "shutdown -h now &"; }
|
|
|
|
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'xdsh $hostlist -v $shutdown_cmd\' \n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run command \'xdsh $hostlist -v $shutdown_cmd\' \n";
|
|
close (RULOG);
|
|
} else {
|
|
xCAT::Utils->runxcmd( { command => ['xdsh'],
|
|
node => \@nodes,
|
|
arg => [ "-v", $shutdown_cmd ]
|
|
}, $::SUBREQ, -1);
|
|
}
|
|
my $slept = 0;
|
|
my $alldown = 1;
|
|
my $nodelist = join( ',', @nodes );
|
|
if (! $::TEST) {
|
|
do {
|
|
$alldown = 1;
|
|
my $shutdownmax = 5;
|
|
if ( defined($::DATAATTRS{shutdowntimeout}[0] ) ) {
|
|
$shutdownmax = $::DATAATTRS{shutdowntimeout}[0];
|
|
}
|
|
my $pwrstat_cmd = "rpower $nodelist stat";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$pwrstat_cmd\' \n";
|
|
close (RULOG);
|
|
}
|
|
my $pwrstat = xCAT::Utils->runxcmd( $pwrstat_cmd, $::SUBREQ, -1, 1 );
|
|
foreach my $pline (@{$pwrstat}) {
|
|
my ( $pnode, $pstat, $rest ) = split( /\s+/, $pline );
|
|
if ( ( ! $pstat )
|
|
|| ( $pstat eq "Running" )
|
|
|| ( $pstat eq "Shutting" )
|
|
|| ( $pstat eq "on" )
|
|
|| ( $pstat eq "Error:" ) )
|
|
{
|
|
|
|
$alldown = 0;
|
|
# give up on shutdown after requested wait time and force the
|
|
# node off
|
|
if ( $slept >= ($shutdownmax * 60) ) {
|
|
$pnode =~ s/://g;
|
|
my $pwroff_cmd = "rpower $pnode off";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: shutdowntimeout exceeded for $pnode \n";
|
|
print RULOG localtime()." $::ug_name: Running command \'$pwroff_cmd\' \n";
|
|
close (RULOG);
|
|
}
|
|
xCAT::TableUtils->setAppStatus([$pnode],"RollingUpdate","shutdowntimeout_exceeded__forcing_rpower_off");
|
|
xCAT::Utils->runxcmd( $pwroff_cmd, $::SUBREQ, -1 );
|
|
} else {
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
|
|
# If all nodes are not down yet, wait some more
|
|
unless ($alldown) {
|
|
sleep(20);
|
|
$slept += 20;
|
|
}
|
|
} until ($alldown);
|
|
} # end not TEST
|
|
|
|
# Run out-of-band commands for this update group
|
|
xCAT::TableUtils->setAppStatus(\@nodes,"RollingUpdate","running_outofbandcmds");
|
|
foreach my $obline ( @{ $::DATAATTRS{'outofbandcmd'} } ) {
|
|
$obline =~ s/\$NODELIST/$hostlist/g;
|
|
# Run the command
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running out-of-band command \'$obline\'\n";
|
|
close (RULOG);
|
|
}
|
|
my @oboutput;
|
|
if ($::TEST) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run out-of-band command \'$obline\'\n";
|
|
close (RULOG);
|
|
} else {
|
|
@oboutput = xCAT::Utils->runcmd( $obline, 0 );
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Out-of-band command output:\n";
|
|
foreach my $oboline (@oboutput) {
|
|
print RULOG $oboline."\n";
|
|
}
|
|
close (RULOG);
|
|
}
|
|
}
|
|
} # end !$skipshutdown
|
|
|
|
|
|
|
|
# reboot the nodes
|
|
# Use bringuporder defined in datafile
|
|
|
|
my $numboots = 0;
|
|
if ( defined($::DATAATTRS{bringuporder}[0]) ) {
|
|
$numboots = scalar( @{$::DATAATTRS{bringuporder}} );
|
|
}
|
|
if ( $skipshutdown ) {
|
|
$numboots = 0;
|
|
}
|
|
my @remaining_nodes = @nodes;
|
|
foreach my $bootindex (0..$numboots){
|
|
my @bootnodes;
|
|
if ((!$skipshutdown) &&
|
|
(defined($::DATAATTRS{bringuporder}[$bootindex]))) {
|
|
@bootnodes = split(/,/,$::DATAATTRS{bringuporder}[$bootindex]);
|
|
foreach my $rn (@remaining_nodes) {
|
|
if ((defined($rn)) && (grep(/^$rn$/,@bootnodes))) {
|
|
undef($rn);
|
|
}
|
|
}
|
|
} else {
|
|
foreach my $rn (@remaining_nodes) {
|
|
if (defined($rn)) {
|
|
push (@bootnodes,$rn);
|
|
undef($rn);
|
|
}
|
|
}
|
|
}
|
|
if (!scalar (@bootnodes)) { next; }
|
|
|
|
# reboot command determined by nodehm power/mgt attributes
|
|
if (!$skipshutdown) {
|
|
my $hmtab = xCAT::Table->new('nodehm');
|
|
my @rpower_nodes;
|
|
my @rnetboot_nodes;
|
|
my $hmtab_entries =
|
|
$hmtab->getNodesAttribs( \@bootnodes, [ 'node', 'mgt', 'power' ] );
|
|
foreach my $node (@bootnodes) {
|
|
my $pwr = $hmtab_entries->{$node}->[0]->{power};
|
|
unless ( defined($pwr) ) { $pwr = $hmtab_entries->{$node}->[0]->{mgt}; }
|
|
if ( $pwr eq 'hmc' ) {
|
|
push( @rnetboot_nodes, $node );
|
|
}
|
|
else {
|
|
push( @rpower_nodes, $node );
|
|
}
|
|
}
|
|
|
|
xCAT::TableUtils->setAppStatus(\@bootnodes,"RollingUpdate","rebooting");
|
|
if ($bootindex < $numboots) {
|
|
xCAT::TableUtils->setAppStatus(\@remaining_nodes,"RollingUpdate","waiting_on_bringuporder");
|
|
}
|
|
if ( scalar(@rnetboot_nodes) > 0 ) {
|
|
my $rnb_nodelist = join( ',', @rnetboot_nodes );
|
|
# my $cmd = "rnetboot $rnb_nodelist -f";
|
|
#### TODO: DO WE STILL NEED 2 LISTS?
|
|
#### RUNNING rpower FOR ALL BOOTS NOW! MUCH FASTER!!!
|
|
my $cmd = "rpower $rnb_nodelist on";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\' \n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run command \'$cmd\' \n";
|
|
close (RULOG);
|
|
} else {
|
|
xCAT::Utils->runxcmd( $cmd, $::SUBREQ, 0 );
|
|
}
|
|
} elsif ( scalar(@rpower_nodes) > 0 ) {
|
|
my $rp_nodelist = join( ',', @rpower_nodes );
|
|
my $cmd = "rpower $rp_nodelist boot";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\' \n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run command \'$cmd\' \n";
|
|
close (RULOG);
|
|
} else {
|
|
xCAT::Utils->runxcmd( $cmd, $::SUBREQ, 0 );
|
|
}
|
|
}
|
|
} # end !$skipshutdown
|
|
|
|
# wait for bringupstatus to be set
|
|
my $not_done = 1;
|
|
my $totalwait = 0;
|
|
my %ll_res;
|
|
while ($not_done && $totalwait < ($statustimeout * 60)) {
|
|
my @query_bootnodes;
|
|
foreach my $bn (keys %ll_res) {
|
|
if ( ! ($ll_res{$bn}{removed}) ) {
|
|
push (@query_bootnodes,$bn);
|
|
}
|
|
}
|
|
if ( ! @query_bootnodes ) {
|
|
@query_bootnodes = @bootnodes;
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Checking ".join(",",@query_bootnodes)." xCAT database $statusattr for value $statusval \n";
|
|
close (RULOG);
|
|
}
|
|
my $nltab_stats =
|
|
$nltab->getNodesAttribs( \@query_bootnodes, [ 'node', $statusattr ] );
|
|
# %ll_res = ();
|
|
$not_done = 0;
|
|
foreach my $bn (@query_bootnodes) {
|
|
if ( $nltab_stats->{$bn}->[0]->{$statusattr} !~ /$statusval/ ) {
|
|
$ll_res{$bn}{not_done}=1;
|
|
$not_done = 1;
|
|
} else {
|
|
$ll_res{$bn}{remove}=1;
|
|
}
|
|
}
|
|
if (($::scheduler eq "loadleveler") &&
|
|
($::ll_reservation_id)){
|
|
my @remove_res_LL;
|
|
my @remove_res_xCAT;
|
|
foreach my $bn (keys %ll_res) {
|
|
if (($ll_res{$bn}{remove}) && (! $ll_res{$bn}{removed}) ){
|
|
if ( defined($::XLATED{$bn}) ) {
|
|
push (@remove_res_LL,$::XLATED{$bn});
|
|
push (@remove_res_xCAT,$bn);
|
|
} else {
|
|
push (@remove_res_LL,$bn);
|
|
push (@remove_res_xCAT,$bn);
|
|
}
|
|
$ll_res{$bn}{removed} = 1;
|
|
$ll_res{$bn}{not_done} = 0;
|
|
}
|
|
}
|
|
if (@remove_res_LL) {
|
|
&remove_LL_reservations(\@remove_res_LL);
|
|
xCAT::TableUtils->setAppStatus(\@remove_res_xCAT,"RollingUpdate","update_complete");
|
|
}
|
|
}
|
|
if ($not_done) {
|
|
if ($::TEST) {
|
|
$not_done = 0;
|
|
} else {
|
|
sleep(20);
|
|
$totalwait += 20;
|
|
}
|
|
}
|
|
}
|
|
if ($not_done) {
|
|
if (($::scheduler eq "loadleveler") &&
|
|
($::ll_reservation_id)){
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG "\n";
|
|
print RULOG localtime()." ERROR: Update group $::ug_name: Reached bringuptimeout before all nodes completed bringup. Some nodes may not have been updated. \n";
|
|
print RULOG "Cancelling LL reservation $::ll_reservation_id \n";
|
|
print RULOG "\n";
|
|
close (RULOG);
|
|
|
|
my @remove_res;
|
|
$remove_res[0]='CANCEL_DUE_TO_ERROR';
|
|
&remove_LL_reservations(\@remove_res);
|
|
my @error_nodes;
|
|
foreach my $bn (keys %ll_res) {
|
|
if ($ll_res{$bn}{not_done}) {
|
|
push (@error_nodes,$bn);
|
|
}
|
|
}
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG "\n";
|
|
print RULOG localtime()." ERROR: bringuptimeout exceeded for the following nodes: \n";
|
|
print RULOG join(",",@error_nodes);
|
|
print RULOG "\n";
|
|
xCAT::TableUtils->setAppStatus(\@error_nodes,"RollingUpdate","ERROR_bringuptimeout_exceeded");
|
|
if ( @remaining_nodes ) {
|
|
my @leftover_nodes;
|
|
foreach my $rn (@remaining_nodes) {
|
|
if (defined($rn)) {
|
|
push (@leftover_nodes,$rn);
|
|
}
|
|
}
|
|
if ( @leftover_nodes ) {
|
|
print RULOG localtime()." ERROR: bringuptimeout exceeded for some nodes in a preceding bringuporder. The following nodes will not be powered on: \n";
|
|
print RULOG join(",",@leftover_nodes);
|
|
print RULOG "\n";
|
|
xCAT::TableUtils->setAppStatus(\@leftover_nodes,"RollingUpdate","ERROR_bringuptimeout_exceeded_for_previous_node");
|
|
}
|
|
}
|
|
close (RULOG);
|
|
}
|
|
last;
|
|
}
|
|
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Rolling update complete.\n\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 readDataFile
|
|
|
|
Process the command line input piped in from a stanza file.
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
Set %::DATAATTRS
|
|
(i.e.- $::DATAATTRS{attr}=[val])
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub readDataFile {
|
|
my $filedata = shift;
|
|
|
|
my $DF;
|
|
unless ( open( $DF, "<", $filedata ) ) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." runrollupdate cannot open datafile $filedata\n";
|
|
close (RULOG);
|
|
return 1;
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." runrollupdate reading datafile $filedata\n";
|
|
close (RULOG);
|
|
}
|
|
my @lines = <$DF>;
|
|
close $DF;
|
|
|
|
foreach my $l (@lines) {
|
|
|
|
# skip blank and comment lines
|
|
next if ( $l =~ /^\s*$/ || $l =~ /^\s*#/ );
|
|
|
|
# process a real line
|
|
if ( $l =~ /^\s*(\w+)\s*=\s*(.*)\s*/ ) {
|
|
my $attr = $1;
|
|
my $val = $2;
|
|
$attr =~ s/^\s*//; # Remove any leading whitespace
|
|
$attr =~ s/\s*$//; # Remove any trailing whitespace
|
|
$attr =~ tr/A-Z/a-z/; # Convert to lowercase
|
|
$val =~ s/^\s*//;
|
|
$val =~ s/\s*$//;
|
|
|
|
# set the value in the hash for this entry
|
|
push( @{ $::DATAATTRS{$attr} }, $val );
|
|
}
|
|
} # end while - go to next line
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 get_hostlist
|
|
|
|
Returns a comma-delimited hostlist string for this updategroup
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub get_hostlist {
|
|
if ( defined($::DATAATTRS{nodelist}[0]) ){
|
|
return $::DATAATTRS{nodelist}[0];
|
|
}
|
|
my $cmd;
|
|
if ($::VERBOSE) {
|
|
$cmd = "llqres -r -s -R $::ll_reservation_id 2>>$::LOGDIR/$::LOGFILE";
|
|
} else {
|
|
$cmd = "llqres -r -s -R $::ll_reservation_id";
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
my ($llstatus) = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
print RULOG localtime()." $llstatus \n";
|
|
close (RULOG);
|
|
}
|
|
if ($::RUNCMD_RC) {
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." ERROR: FAILED calling: \n $cmd \n";
|
|
close (RULOG);
|
|
}
|
|
return 0;
|
|
} else {
|
|
my @status_fields = split(/!/,$llstatus);
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Hostlist: $status_fields[22] \n";
|
|
close (RULOG);
|
|
}
|
|
my $return_list;
|
|
foreach my $machine (split( /\,/, $status_fields[22])) {
|
|
if ( defined($::XLATED{$machine}) ) {
|
|
$return_list = $return_list.','.$::XLATED{$machine};
|
|
} else {
|
|
$return_list = $return_list.','.$machine;
|
|
}
|
|
}
|
|
$return_list =~ s/^,//;
|
|
return $return_list;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 remove_LL_reservations
|
|
|
|
Changes the LL feature for nodes from oldfeature to newfeature
|
|
Remove nodes from LL reservation
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub remove_LL_reservations {
|
|
my $input_nodes = shift;
|
|
my $nodes = $input_nodes;
|
|
my $CANCEL_DUE_TO_ERROR = 0;
|
|
if ( $input_nodes->[0] eq 'CANCEL_DUE_TO_ERROR') {
|
|
$CANCEL_DUE_TO_ERROR = 1;
|
|
}
|
|
|
|
my $cmd;
|
|
if ($::VERBOSE) {
|
|
$cmd = "llqres -r -R $::ll_reservation_id 2>>$::LOGDIR/$::LOGFILE";
|
|
} else {
|
|
$cmd = "llqres -r -R $::ll_reservation_id";
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
my $nl = join(",",@{$nodes});
|
|
print RULOG localtime()." $::ug_name: remove_LL_reservations for $nl \n";
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
my ($llstatus) = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
print RULOG localtime()." $llstatus \n";
|
|
close (RULOG);
|
|
}
|
|
if ( $::RUNCMD_RC ) {
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." ERROR: FAILED calling:\n $cmd \n";
|
|
close (RULOG);
|
|
}
|
|
return 0;
|
|
}
|
|
my @status_fields = split(/!/,$llstatus);
|
|
my $llnodelist = $status_fields[22];
|
|
my @llnodes = split(/,/,$llnodelist);
|
|
my $llnode_count = scalar (@llnodes);
|
|
my $remove_count = 0;
|
|
my $remove_reservation = 0;
|
|
my $remove_cmd = "llchres -R $::ll_reservation_id -h -";
|
|
|
|
if ($CANCEL_DUE_TO_ERROR) {
|
|
$nodes = \@llnodes;
|
|
}
|
|
my @llnodes_removed;
|
|
foreach my $n (@{$nodes}) {
|
|
# change features for this node
|
|
if ($CANCEL_DUE_TO_ERROR) {
|
|
&remove_LL_updatefeature_only($n);
|
|
} else {
|
|
&change_LL_feature($n);
|
|
}
|
|
my @lln;
|
|
if ( (@lln=grep(/^$n$/,@llnodes)) | (@lln=grep(/^$n\./,@llnodes)) ) {
|
|
$remove_count++;
|
|
push (@llnodes_removed,$lln[0]);
|
|
if ( $remove_count < $llnode_count ) {
|
|
$remove_cmd .= " $lln[0]";
|
|
} else {
|
|
$remove_reservation = 1;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
llreconfig();
|
|
# Verify that the config change has been registered and that updatefeature
|
|
# has been removed according to what the LL daemons report
|
|
if (defined($::DATAATTRS{updatefeature}[0])) {
|
|
my $machinelist = join(" ",@llnodes_removed);
|
|
my $llset;
|
|
my $statrun = 0;
|
|
do {
|
|
sleep 1;
|
|
my $llstatus = "llstatus -Lmachine -h $machinelist -l | grep -i feature | grep -i \" $::DATAATTRS{updatefeature}[0] \"";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command \'$llstatus\'\n";
|
|
close (RULOG);
|
|
}
|
|
my @stat_out = xCAT::Utils->runcmd( $llstatus, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
# Are there any nodes with this feature still set?
|
|
$llset = $stat_out[0];
|
|
### DEBUG - print output to see what's going on
|
|
#if ($llset) {
|
|
# open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
# print RULOG localtime()." llset: $llset\n";
|
|
# close (RULOG);
|
|
#}
|
|
### end DEBUG
|
|
$statrun ++;
|
|
} until ((! $llset) || ($statrun > 10) );
|
|
}
|
|
|
|
if ($remove_reservation) {
|
|
$cmd = "llrmres -R $::ll_reservation_id";
|
|
} elsif ( $remove_count > 0 ) {
|
|
$cmd = $remove_cmd;
|
|
} else {
|
|
return 0; # no LL nodes to remove
|
|
}
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
# if ($::TEST) {
|
|
# open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
# print RULOG localtime()." $::ug_name: In TEST mode. Will NOT run command \'$cmd\' \n";
|
|
# close (RULOG);
|
|
# } else {
|
|
xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ( $::RUNCMD_RC != 0 ) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Error running command \'$cmd\'\n";
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
return 1;
|
|
}
|
|
# }
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 change_LL_feature
|
|
|
|
Changes the LL feature for the node from oldfeature to newfeature
|
|
and removes updatefeature
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub change_LL_feature {
|
|
my $node = shift;
|
|
|
|
if (!defined($::DATAATTRS{updatefeature}[0]) &&
|
|
!defined($::DATAATTRS{oldfeature}[0]) &&
|
|
!defined($::DATAATTRS{newfeature}[0]) ) {
|
|
return 0;
|
|
}
|
|
# Query current feature
|
|
my $cmd = "llconfig -h $node -d FEATURE";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
my ($llcfgout) = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
# Remove old feature
|
|
my $newfeature_string = "";
|
|
my @llfeatures;
|
|
if ( $llcfgout =~ /:FEATURE =/ ) {
|
|
my ($stuff,$curfeature_string) = split(/=/,$llcfgout);
|
|
@llfeatures = split(/\s+/,$curfeature_string);
|
|
my $oldfeature = " ";
|
|
my $updateallfeature = " ";
|
|
if (defined($::DATAATTRS{oldfeature}[0])) {
|
|
$oldfeature = $::DATAATTRS{oldfeature}[0];
|
|
}
|
|
if (defined($::DATAATTRS{updatefeature}[0])) {
|
|
$updateallfeature = $::DATAATTRS{updatefeature}[0];
|
|
}
|
|
foreach my $f (@llfeatures) {
|
|
if (($f eq $oldfeature) || ($f eq $updateallfeature)) {
|
|
$f = " ";
|
|
}
|
|
}
|
|
$newfeature_string = join(" ",@llfeatures);
|
|
}
|
|
|
|
# Add new feature
|
|
if ( defined($::DATAATTRS{newfeature}[0]) ) {
|
|
my $haveit = 0;
|
|
foreach my $f (@llfeatures) {
|
|
if ($f eq $::DATAATTRS{newfeature}[0]){
|
|
$haveit = 1;
|
|
}
|
|
}
|
|
if ( !$haveit) {
|
|
$newfeature_string .= " $::DATAATTRS{newfeature}[0]";
|
|
}
|
|
}
|
|
# Change in LL database
|
|
$cmd = "llconfig -N -h $node -c FEATURE=\"$newfeature_string\"";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
# llreconfig();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 remove_LL_updatefeature_only
|
|
|
|
Changes the LL feature for the node to remove only the updatefeature
|
|
Will NOT remove oldfeature or set newfeature
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub remove_LL_updatefeature_only {
|
|
my $node = shift;
|
|
|
|
if (!defined($::DATAATTRS{updatefeature}[0]) ) {
|
|
return 0;
|
|
}
|
|
# Query current feature
|
|
my $cmd = "llconfig -h $node -d FEATURE";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
my ($llcfgout) = xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
# Remove old feature
|
|
my $newfeature_string = "";
|
|
my @llfeatures;
|
|
if ( $llcfgout =~ /:FEATURE =/ ) {
|
|
my ($stuff,$curfeature_string) = split(/=/,$llcfgout);
|
|
@llfeatures = split(/\s+/,$curfeature_string);
|
|
my $updateallfeature = " ";
|
|
if (defined($::DATAATTRS{updatefeature}[0])) {
|
|
$updateallfeature = $::DATAATTRS{updatefeature}[0];
|
|
}
|
|
foreach my $f (@llfeatures) {
|
|
if ($f eq $updateallfeature) {
|
|
$f = " ";
|
|
}
|
|
}
|
|
$newfeature_string = join(" ",@llfeatures);
|
|
}
|
|
|
|
# Change in LL database
|
|
$cmd = "llconfig -N -h $node -c FEATURE=\"$newfeature_string\"";
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." $::ug_name: Running command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
xCAT::Utils->runcmd( $cmd, 0 );
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Return code: $::RUNCMD_RC\n";
|
|
close (RULOG);
|
|
}
|
|
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
# llreconfig();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
=head3 llreconfig
|
|
|
|
Queries LoadLeveler for the list of central mgrs and resource mgrs
|
|
and sends a llrctl reconfig to those nodes
|
|
|
|
Arguments:
|
|
Returns:
|
|
0 - OK
|
|
1 - error
|
|
Globals:
|
|
Error:
|
|
Example:
|
|
|
|
Comments:
|
|
|
|
=cut
|
|
|
|
#-----------------------------------------------------------------------------
|
|
sub llreconfig {
|
|
|
|
# Send LL reconfig to all central mgrs and resource mgrs
|
|
my $cmd = "llconfig -d CENTRAL_MANAGER_LIST RESOURCE_MGR_LIST";
|
|
my @llcfg_d = xCAT::Utils->runcmd( $cmd, 0 );
|
|
my $llcms = "";
|
|
my $llrms = "";
|
|
foreach my $cfgo (@llcfg_d) {
|
|
chomp $cfgo;
|
|
my($llattr,$llval) = split (/ = /,$cfgo);
|
|
if ( $llattr =~ /CENTRAL_MANAGER_LIST/ ) {
|
|
$llcms = $llval; }
|
|
if ( $llattr =~ /RESOURCE_MGR_LIST/ ) {
|
|
$llrms = $llval; }
|
|
}
|
|
$cmd = "llrctl reconfig";
|
|
my @llms = split(/\s+/,$llcms." ".$llrms);
|
|
my %have = ();
|
|
my @llnodes;
|
|
my $runlocal=1; # need to always run reconfig at least on local MN
|
|
foreach my $m (@llms) {
|
|
my ($sm,$rest) = split(/\./,$m);
|
|
my $xlated_sm = $sm;
|
|
if ( defined ($::XLATED{$sm}) ) { $xlated_sm = $::XLATED{$sm}; }
|
|
if (xCAT::NetworkUtils->thishostisnot($m)) {
|
|
push(@llnodes, $xlated_sm) unless $have{$sm}++;
|
|
} else {
|
|
$runlocal=1;
|
|
}
|
|
}
|
|
if ( defined($::FILEATTRS{reconfiglist}[0]) ) {
|
|
push (@llnodes, split( /,/,$::FILEATTRS{reconfiglist}[0]) );
|
|
} elsif ( defined($::DATAATTRS{reconfiglist}[0]) ) {
|
|
push (@llnodes, split( /,/,$::DATAATTRS{reconfiglist}[0]) );
|
|
}
|
|
if ($runlocal) {
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running local command \'$cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "In TEST mode. Will NOT run command: $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
$::RUNCMD_RC = 0;
|
|
} else {
|
|
xCAT::Utils->runcmd( $cmd, 0 );
|
|
}
|
|
}
|
|
|
|
if ( scalar(@llnodes) > 0 ) {
|
|
if ($::VERBOSE) {
|
|
open (RULOG, ">>$::LOGDIR/$::LOGFILE");
|
|
print RULOG localtime()." Running command \'xdsh ".join(',',@llnodes)." $cmd\'\n";
|
|
close (RULOG);
|
|
}
|
|
if ($::TEST) {
|
|
my $rsp;
|
|
push @{ $rsp->{data} }, "In TEST mode. Will NOT run command: xdsh <llcm,llrm> $cmd ";
|
|
xCAT::MsgUtils->message( "I", $rsp, $::CALLBACK );
|
|
$::RUNCMD_RC = 0;
|
|
} else {
|
|
xCAT::Utils->runxcmd(
|
|
{ command => ['xdsh'],
|
|
node => \@llnodes,
|
|
arg => [ "-v", $cmd ]
|
|
},
|
|
$::SUBREQ, -1);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
1;
|