#!/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;
