# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
#####################################################
#
#  xCAT plugin package to handle various commands that work with the
#     xCAT tables
#
#
#####################################################
package xCAT_plugin::tabutils;
use strict;
use warnings;
use xCAT::Table;
use xCAT::Schema;
use Data::Dumper;
use xCAT::NodeRange qw/noderange abbreviate_noderange/;
use xCAT::Schema;
use xCAT::Utils;

#use XML::Simple;
use xCAT::TableUtils;
use xCAT::MsgUtils;
use xCAT::DBobjUtils;
use Getopt::Long;
my $requestcommand;

1;

#some quick aliases to table/value
my %shortnames = (
    groups => [qw(nodelist groups)],
    tags   => [qw(nodelist groups)],
    mgt    => [qw(nodehm mgt)],

    #switch => [qw(switch switch)],
);

#####################################################
# Return list of commands handled by this plugin
#####################################################
sub handled_commands
{
    return {
        gettab     => "tabutils",
        tabdump    => "tabutils",
        lsxcatd    => "tabutils",
        tabprune   => "tabutils",
        tabrestore => "tabutils",
        tabch      => "tabutils",
        nodegrpch  => "tabutils",
        nodech     => "tabutils",
        nodeadd    => "tabutils",
        noderm     => "tabutils",
        tabls      => "tabutils",    # not implemented yet
        nodels     => "tabutils",
        getnodecfg => "tabutils", # not implemented yet (?? this doesn't seem much different from gettab)
        addattr                 => "tabutils",    # not implemented yet
        delattr                 => "tabutils",    # not implemented yet
        chtype                  => "tabutils",    # not implemented yet
        nr                      => "tabutils",    # not implemented yet
        rnoderange              => "tabutils",    # not implemented yet
        tabgrep                 => "tabutils",
        getAllEntries           => "tabutils",
        getNodesAttribs         => "tabutils",
        getTablesAllNodeAttribs => "tabutils",
        getTablesNodesAttribs   => "tabutils",
        getTablesAllRowAttribs  => "tabutils",
        setNodesAttribs         => "tabutils",
        delEntries              => "tabutils",
        getAttribs              => "tabutils",
        setAttribs              => "tabutils",
        NodeRange               => "tabutils",
        gennr                   => "tabutils"
    };
}

# Each cmd now returns its own usage inside its function

#####################################################
# Process the command
#####################################################
sub process_request
{
    #use Getopt::Long;
    Getopt::Long::Configure("bundling");

    #Getopt::Long::Configure("pass_through");
    Getopt::Long::Configure("no_pass_through");

    my $request  = shift;
    my $callback = shift;
    $requestcommand = shift;
    my $nodes   = $request->{node};
    my $command = $request->{command}->[0];
    my $args    = $request->{arg};

    #unless ($args or $nodes or $request->{data})
    #{
    #if ($usage{$command})
    #{
    #$callback->({data => [$usage{$command}]});
    #return;
    #}
    #}

    if ($command eq "nodels")
    {
        return nodels($nodes, $args, $callback, $request->{emptynoderange}->[0]);
    }
    elsif ($command eq "rnoderange")
    {
        return rnoderange($nodes, $args, $callback);
    }
    elsif ($command eq "noderm" or $command eq "rmnode")
    {
        return noderm($nodes, $args, $callback);
    }
    elsif ($command eq "nodeadd" or $command eq "addnode")
    {
        return nodech($nodes, $args, $callback, 1);
    }
    elsif ($command eq "nodegrpch" or $command eq "chnodegrp")
    {
        return nodech($nodes, $args, $callback, "groupch");
    }
    elsif ($command eq "gennr")
    {
        return gennr($nodes, $args, $callback);
    }
    elsif ($command eq "nodech" or $command eq "chnode")
    {
        return nodech($nodes, $args, $callback, 0);
    }
    elsif ($command eq "tabrestore")
    {
        return tabrestore($request, $callback);
    }
    elsif ($command eq "tabdump")
    {
        return tabdump($args, $callback, $request);
    }
    elsif ($command eq "lsxcatd")
    {
        return lsxcatd($args, $callback);
    }
    elsif ($command eq "tabprune")
    {
        return tabprune($args, $callback);
    }
    elsif ($command eq "gettab")
    {
        return gettab($request, $callback);
    }
    elsif ($command eq "tabgrep")
    {
        return tabgrep($nodes, $callback);
    }
    elsif ($command eq "tabch") {
        return tabch($request, $callback);
    }
    elsif ($command eq "getAllEntries")
    {
        return getAllEntries($request, $callback);
    }
    elsif ($command eq "getNodesAttribs")
    {
        return getNodesAttribs($request, $callback);
    }
    elsif ($command eq "getTablesAllNodeAttribs")
    {
        return getTablesAllNodeAttribs($request, $callback);
    }
    elsif ($command eq "getTablesNodesAttribs")
    {
        return getTablesNodesAttribs($request, $callback);
    }
    elsif ($command eq "getTablesAllRowAttribs")
    {
        return getTablesAllRowAttribs($request, $callback);
    }
    elsif ($command eq "setNodesAttribs")
    {
        return setNodesAttribs($request, $callback);
    }
    elsif ($command eq "delEntries")
    {
        return delEntries($request, $callback);
    }
    elsif ($command eq "getAttribs")
    {
        return getAttribs($request, $callback);
    }
    elsif ($command eq "setAttribs")
    {
        return setAttribs($request, $callback);
    }
    elsif ($command eq "NodeRange")
    {
        return NodeRange($request, $callback);
    }
    else
    {
        print "$command not implemented yet\n";
        return (1, "$command not written yet");
    }

}

# Display particular attributes, using query strings.
sub gettab
{
    my $req      = shift;
    my $callback = shift;
    my $HELP;
    my $NOTERSE;

    my $gettab_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage: gettab [-H|--with-fieldname] key=value,...  table.attribute ...";
        push @{ $rsp{data} }, "       gettab [-?|-h|--help]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $callback->(\%rsp);
    };

    # Process arguments
    if (!defined($req->{arg})) { $gettab_usage->(1); return; }
    @ARGV = @{ $req->{arg} };
    if (!GetOptions('h|?|help' => \$HELP, 'H|with-fieldname' => \$NOTERSE)) { $gettab_usage->(1); return; }

    if ($HELP)             { $gettab_usage->(0); return; }
    if (scalar(@ARGV) < 2) { $gettab_usage->(1); return; }

    # Get all the key/value pairs into a hash
    my $keyspec = shift @ARGV;
    my @keypairs = split /,/, $keyspec;
    my %keyhash;
    foreach (@keypairs)
    {
        (my $key, my $value) = split /=/, $_;
        unless (defined $key) {
            $gettab_usage->(1);
            return;
        }
        $keyhash{$key} = $value;
    }

    # Group the columns asked for by table (so we can do 1 query per table)
    my %tabhash;
    my $terse = 2;
    if ($NOTERSE) {
        $terse = 0;
    }
    foreach my $tabvalue (@ARGV)
    {
        $terse--;
        (my $table, my $column) = split /\./, $tabvalue;
        $tabhash{$table}->{$column} = 1;
    }

    #Sanity check the key against all tables in question
    foreach my $tabn (keys %tabhash) {
        foreach my $kcheck (keys %keyhash) {
            unless (grep /^$kcheck$/, @{ $xCAT::Schema::tabspec{$tabn}->{cols} }) {
                $callback->({ error => ["Unkown key $kcheck to $tabn"], errorcode => [1] });
                return;
            }
        }
    }

    # Get the requested columns from each table
    foreach my $tabn (keys %tabhash)
    {
        my $tab = xCAT::Table->new($tabn);
        (my $ent) = $tab->getAttribs(\%keyhash, keys %{ $tabhash{$tabn} });
        if ($ent) {
            foreach my $coln (keys %{ $tabhash{$tabn} })
            {
                if ($terse > 0) {
                    $callback->({ data => [ "" . $ent->{$coln} ] });
                } else {
                    $callback->({ data => [ "$tabn.$coln: " . $ent->{$coln} ] });
                }
            }
        }
        $tab->close;
    }
}

sub noderm
{
    my $nodes = shift;
    my $args  = shift;
    my $cb    = shift;
    my $VERSION;
    my $HELP;

    my $noderm_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage:";
        push @{ $rsp{data} }, "  noderm noderange";
        push @{ $rsp{data} }, "  noderm {-v|--version}";
        push @{ $rsp{data} }, "  noderm [-?|-h|--help]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $cb->(\%rsp);
    };

    if ($args) {
        @ARGV = @{$args};
    } else {
        @ARGV = ();
    }
    if (!GetOptions('h|?|help' => \$HELP, 'v|version' => \$VERSION)) { $noderm_usage->(1); return; }

    if ($HELP) { $noderm_usage->(0); return; }

    if ($VERSION) {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $cb->(\%rsp);
        return;
    }

    if (!$nodes) { $noderm_usage->(1); return; }

    #my $sitetab = xCAT::Table->new('site');
    #my $pdhcp = $sitetab->getAttribs({key=>'pruneservices'},['value']);
    my @entries = xCAT::TableUtils->get_site_attribute("pruneservices");
    my $t_entry = $entries[0];
    if (defined($t_entry) and $t_entry !~ /n(\z|o)/i) {
        $requestcommand->({ command => ['makedhcp'], node => $nodes, arg => ['-d'] });
    }



    # Build the argument list for using the -d option of nodech to do our work for us
    my @tablist = ("-d");
    foreach (keys %{xCAT::Schema::tabspec})
    {
        if (grep /^node$/, @{ $xCAT::Schema::tabspec{$_}->{cols} })
        {
            push @tablist, $_;
        }
    }
    if (scalar(@$nodes)) {
        for my $nn (@$nodes) {
            my $nt = xCAT::DBobjUtils->getnodetype($nn);
            if ($nt and $nt =~ /^(cec|frame)$/) {
                my $cnodep = xCAT::DBobjUtils->getchildren($nn);
                if ($cnodep) {
                    my $cnode = join ',', @$cnodep;
                    if ($cnode)
                    {
                        my $rsp;
                        $rsp->{data}->[0] =
"Removed a $nt node, please remove these nodes belongs to it manually: $cnode \n";
                        xCAT::MsgUtils->message("I", $rsp, $cb);
                    }
                }
            }
        }
    }

    nodech($nodes, \@tablist, $cb, 0);
}
#
# restores the table from the input CSV file.  Default deletes the table rows and
# replaces with the rows in the file
# If -a flag is input then it adds the rows from the CSV file to the table.
#
sub tabrestore
{
    # the usage for tabrestore is in the tabrestore client cmd

    #request->{data} is an array of CSV formatted lines
    my $request = shift;
    my $cb      = shift;
    my $table   = $request->{table}->[0];
    my $addrows = $request->{addrows}->[0];

    # do not allow teal tables
    if ($table =~ /^x_teal/) {
        $cb->({ error => "$table is not supported in tabrestore. Use Teal maintenance commands. ", errorcode => 1 });
        return 1;
    }
    my $tab = xCAT::Table->new($table, -create => 1, -autocommit => 0);
    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return;
    }
    if (!defined($addrows)) {    # this is a replace not add rows
        $tab->delEntries();      #Yes, delete *all* entries
    }
    my $header = shift @{ $request->{data} };
    unless ($header =~ /^#/) {
        $cb->({ error => "Data missing header line starting with #", errorcode => 1 });
        return;
    }
    $header =~ s/"//g;           #Strip " from overzealous CSV apps
    $header =~ s/^#//;
    $header =~ s/\s+$//;
    my @colns = split(/,/, $header);
    my $tcol;
    foreach $tcol (@colns) { #validate the restore data has no invalid column names
        unless (grep /^$tcol\z/, @{ $xCAT::Schema::tabspec{$table}->{cols} }) {
            $cb->({ error => "The header line indicates that column '$tcol' should exist, which is not defined in the schema for '$table'", errorcode => 1 });
            return;
        }

        #print Dumper(grep /^$tcol\z/,@{$xCAT::Schema::tabspec{$table}->{cols}});
    }

    #print "We passed it!\n";
    my $line;
    my $rollback = 0;

    my @tmp = $tab->getAutoIncrementColumns(); #get the columns that are auto increment by DB.
    my %auto_cols = ();
    foreach (@tmp) { $auto_cols{$_} = 1; }


    my $linenumber;
    my $linecount = scalar(@{ $request->{data} });

  LINE: for ($linenumber = 0 ; $linenumber < $linecount ; $linenumber++)
    {
        $line = @{ $request->{data} }[$linenumber];
        $line =~ s/\s+$//;
        my $origline = $line;                  #save for error reporting
        my %record;
        my $col;
        foreach $col (@colns)
        {
            if ($line =~ /^,/ or $line eq "")
            {    #Match empty, or end of line that is empty
                 #TODO: should we detect when there weren't enough CSV fields on a line to match colums?
                if (!exists($auto_cols{$col})) {
                    $record{$col} = undef;
                }
                $line =~ s/^,//;
            }
            elsif ($line =~ /^[^,]*"/)
            {    # We have stuff in quotes... pain...
                    #I don't know what I'm doing, so I'll do it a hard way....
                if ($line !~ /^"/)
                {
                    $rollback = 1;
                    $cb->(
                        {
                            error =>
"CSV missing opening \" for record with \" characters on line $linenumber, character "
                              . index($origline, $line) . ": $origline", errorcode => 4
                        }
                    );
                    next LINE;
                }
                my $offset = 1;
                my $nextchar;
                my $ent;
                while (not defined $ent)
                {
                    $offset = index($line, '"', $offset);
                    $offset++;

                    if ($offset <= 0)
                    {    #the matching quote is not on this line of the file

                        if ($linenumber < $linecount)
                        { #it's not the end of the world, we have more lines to check

                            my $continuedline = @{ $request->{data} }[ ++$linenumber ];
                            $offset = length($line);
                            $line .= "\n" . $continuedline;
                            $line =~ s/\s+$//;
                        }
                        else
                        { #the matching quote was not found before the end of the file

                            #MALFORMED CSV, request rollback, report an error
                            $rollback = 1;
                            $cb->(
                                {
                                    error =>
"CSV unmatched \" in record on line $linenumber, character "
                                      . index($origline, $line) . ": $origline", errorcode => 4
                                }
                            );
                            next LINE;
                        }
                    }
                    else
                    {    #the next quote was on the current line

                        $nextchar = substr($line, $offset, 1);

                        if ($nextchar eq '"')
                        { #the case of 2 double quotes.  ignore them and move on
                            $offset++;
                        }
                        elsif ($offset eq length($line) or $nextchar eq ',')
                        { #hit the end of the line or at least the end of the column
                            $ent = substr($line, 0, $offset, '');
                            $line =~ s/^,//;
                            chop $ent;
                            $ent = substr($ent, 1);
                            $ent =~ s/""/"/g;
                            if (!exists($auto_cols{$col}))
                            {
                                $record{$col} = $ent;
                            }
                        }
                        else
                        {
                            $cb->(
                                {
                                    error =>
"CSV unescaped \" in record on line $linenumber, character "
                                      . index($origline, $line) . ": $origline", errorcode => 4
                                }
                            );
                            $rollback = 1;
                            next LINE;
                        }
                    }
                }
            }
            elsif ($line =~ /^([^,]+)/)
            {    #easiest case, no Text::Balanced needed..
                if (!exists($auto_cols{$col}))
                {
                    $record{$col} = $1;
                }
                $line =~ s/^([^,]+)(,|$)//;
            }
        }
        if ($line)
        {
            $rollback = 1;
            $cb->({ error => "Too many fields on line $linenumber: $origline | $line", errorcode => 4 });
            next LINE;
        }

        #check for error from DB and rollback
        my @rc = $tab->setAttribs(\%record, \%record);
        if (not defined($rc[0]))
        {
            $rollback = 1;
            $cb->({ error => "DB error " . $rc[1] . " with line $linenumber: " . $origline, errorcode => 4 });
        }
    }
    if ($rollback)
    {
        $tab->rollback();
        $tab->close;
        undef $tab;
        return;
    }
    else
    {
        $tab->commit;    #Made it all the way here, commit
    }
}

# Display a list of tables, or a specific table in CSV format
sub tabdump
{
    my $args    = shift;
    my $cb      = shift;
    my $request = shift;
    my $table   = "";
    my $HELP;
    my $DESC;
    my $OPTW;
    my $VERSION;
    my $FILENAME;
    my $NUMBERENTRIES;

    my $tabdump_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage: tabdump [-d] [table]";
        push @{ $rsp{data} }, "       tabdump      [table]";
        push @{ $rsp{data} }, "       tabdump [-f <filename>]  [table]";
        push @{ $rsp{data} }, "       tabdump [-n <# of records>]  [auditlog | eventlog]";
        push @{ $rsp{data} }, "       tabdump [-w attr==val [-w attr=~val] ...] [table]";
        push @{ $rsp{data} }, "       tabdump [-w attr==val [-w attr=~val] ...] [-f <filename>] [table]";
        push @{ $rsp{data} }, "       tabdump [-?|-h|--help]";
        push @{ $rsp{data} }, "       tabdump {-v|--version}";
        push @{ $rsp{data} }, "       tabdump ";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $cb->(\%rsp);
    };

    # Process arguments
    if ($args) {
        @ARGV = @{$args};
    }
    Getopt::Long::Configure("posix_default");
    Getopt::Long::Configure("no_gnu_compat");
    Getopt::Long::Configure("bundling");


    if (!GetOptions(
            'h|?|help'  => \$HELP,
            'v|version' => \$VERSION,
            'n|lines=i' => \$NUMBERENTRIES,
            'd'         => \$DESC,
            'f=s'       => \$FILENAME,
            'w=s@'      => \$OPTW,
        )
      )
    { $tabdump_usage->(1);
        return;
    }
    if ($VERSION) {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $cb->(\%rsp);
        return;
    }
    if ($FILENAME and $FILENAME !~ /^\//) { $FILENAME =~ s/^/$request->{cwd}->[0]\//; }


    if ($HELP) { $tabdump_usage->(0); return; }

    if (($NUMBERENTRIES) && ($DESC)) {
        $cb->({ error => "You  cannot use the -n and -d flag together. ", errorcode => 1 });
        return 1;
    }

    if (($NUMBERENTRIES) && ($OPTW)) {
        $cb->({ error => "You  cannot use the -n and -w flag together. ", errorcode => 1 });
        return 1;
    }
    if (($NUMBERENTRIES) && ($FILENAME)) {
        $cb->({ error => "You  cannot use the -n and -f flag together. ", errorcode => 1 });
        return 1;
    }
    if (scalar(@ARGV) > 1) { $tabdump_usage->(1); return; }

    my %rsp;

    # If no arguments given, we display a list of the tables
    if (!scalar(@ARGV)) {

        # if -f filename give but no table name, display error
        if ($FILENAME) {
            $cb->({ error => "table name missing from the command input. ", errorcode => 1 });
            return 1;
        }
        if ($DESC) {    # display the description of each table
            my $tab = xCAT::Table->getDescriptions();
            foreach my $key (keys %$tab) {
                my $space = (length($key) < 7 ? "\t\t" : "\t");
                push @{ $rsp{data} }, "$key:$space" . $tab->{$key} . "\n";
            }
        }
        else { push @{ $rsp{data} }, xCAT::Table->getTableList(); } # if no descriptions, just display the list of table names
        @{ $rsp{data} } = sort @{ $rsp{data} };
        if ($DESC && scalar(@{ $rsp{data} })) { chop($rsp{data}->[ scalar(@{ $rsp{data} }) - 1 ]); } # remove the final newline
        $cb->(\%rsp);
        return;
    }

    # get the table name
    $table = $ARGV[0];

    # if -n  can only be the auditlog or eventlog
    if ($NUMBERENTRIES) {
        if (!($table =~ /^auditlog/) && (!($table =~ /^eventlog/))) {
            $cb->({ error => "$table table is not supported in tabdump -n. You may only use this option on the auditlog or the eventlog.", errorcode => 1 });
            return 1;
        }
    }

    # do not allow teal tables
    if ($table =~ /^x_teal/) {
        $cb->({ error => "$table table is not supported in tabdump. Use Teal maintenance commands. ", errorcode => 1 });
        return 1;
    }
    if ($DESC) {    # only show the attribute descriptions, not the values
        my $schema = xCAT::Table->getTableSchema($table);
        if (!$schema) { $cb->({ error => "table $table does not exist.", errorcode => 1 }); return; }
        my $desc = $schema->{descriptions};
        foreach my $c (@{ $schema->{cols} }) {
            my $space = (length($c) < 7 ? "\t\t" : "\t");
            push @{ $rsp{data} }, "$c:$space" . $desc->{$c} . "\n";
        }
        if (scalar(@{ $rsp{data} })) { chop($rsp{data}->[ scalar(@{ $rsp{data} }) - 1 ]); } # remove the final newline
        $cb->(\%rsp);
        return;
    }


    my $tabh = xCAT::Table->new($table);

    my $tabdump_header = sub {
        my $header = "#" . join(",", @_);
        push @{ $rsp{data} }, $header;
    };

    # If the table does not exist yet (because its never been written to),
    # at least show the header (the column names)
    unless ($tabh)
    {
        if (defined($xCAT::Schema::tabspec{$table}))
        {
            $tabdump_header->(@{ $xCAT::Schema::tabspec{$table}->{cols} });
            $cb->(\%rsp);
            return;
        }
        $cb->({ error => "No such table: $table", errorcode => 1 });
        return 1;
    }
    #
    # if tabdump -n <number of recs> auditlog|eventlog
    #
    if (defined $NUMBERENTRIES) {
        my $rc = tabdump_numberentries($table, $cb, $NUMBERENTRIES);
        return $rc;
    }

    my $recs;
    my @ents;
    my @attrarray;
    if (!($FILENAME)) {    # not dumping to a file
        if (!($OPTW)) {    # if no -w flag to filter, then get all
            $recs = $tabh->getAllEntries("all");
        } else {           # filter entries
            foreach my $w (@{$OPTW}) {    # get each attr=val
                push @attrarray, $w;
            }
            @ents = $tabh->getAllAttribsWhere(\@attrarray, 'ALL');
            @$recs = ();
            foreach my $e (@ents) {
                push @$recs, $e;
            }
        }
        my $rec;
        unless (@$recs)    # table exists, but is empty.  Show header.
        {
            if (defined($xCAT::Schema::tabspec{$table}))
            {
                $tabdump_header->(@{ $xCAT::Schema::tabspec{$table}->{cols} });
                $cb->(\%rsp);
                return;
            }
        }

        #Display all the rows of the table  the order of the columns in the schema
        output_table($table, $cb, $tabh, $recs);
    } else {               # dump to file

        my $rc1 = 0;
        my $fh;

        # check to see if you can open the file
        unless (open($fh, " > $FILENAME")) {
            $cb->({ error => "Error on tabdump of $table to $FILENAME. Unable to open the file for write. ", errorcode => 1 });
            return 1;
        }
        close $fh;
        if (!($OPTW)) {    # if no -w flag to filter, then get all
            $rc1 = $tabh->writeAllEntries($FILENAME);
        } else {           # filter entries
            foreach my $w (@{$OPTW}) {    # get each attr=val
                push @attrarray, $w;
            }
            $rc1 = $tabh->writeAllAttribsWhere(\@attrarray, $FILENAME);
        }
        if ($rc1 != 0) {
            $cb->({ error => "Error on tabdump of $table to $FILENAME ", errorcode => 1 });
            return 1;
        }
    }
}
#
#  display input number of records for the table requested tabdump -n
#  note currently only supports auditlog and eventlog
#
sub tabdump_numberentries {
    my $table         = shift;
    my $cb            = shift;
    my $numberentries = shift;    # either number of records to display

    my $VERBOSE = shift;
    my $rc      = 0;
    my $tab     = xCAT::Table->new($table);
    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return 1;
    }

    #determine recid to show all records after
    my $RECID;
    my $attrrecid = "recid";
    my $values    = $tab->getMAXMINEntries($attrrecid);
    my $max       = $values->{"max"};
    if (defined($values->{"max"})) {
        $RECID = $values->{"max"} - $numberentries;
        $rc = tabdump_recid($table, $cb, $RECID, $attrrecid);

    } else {
        my %rsp;
        push @{ $rsp{data} }, "Nothing to display from $table.";
        $rsp{errorcode} = $rc;
        $cb->(\%rsp);
    }
    return $rc;
}

#  Display requested recored
#  if rec id  does not exist error
sub tabdump_recid {
    my $table = shift;
    my $cb    = shift;
    my $recid = shift;
    my $rc    = 0;

    # check which database so can build the correct Where clause
    my $tab = xCAT::Table->new($table);
    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return 1;
    }
    my $DBname = xCAT::Utils->get_DBName;
    my @recs;
    my $attrrecid = "recid";

    # display the output
    if ($DBname =~ /^DB2/) {
        @recs = $tab->getAllAttribsWhere("\"$attrrecid\">$recid", 'ALL');
    } else {
        @recs = $tab->getAllAttribsWhere("$attrrecid>$recid", 'ALL');
    }
    output_table($table, $cb, $tab, \@recs);
    return $rc;
}

# Display information from the daemon.
#
sub lsxcatd
{
    my $args = shift;
    my $cb   = shift;
    my $HELP;
    my $VERSION;
    my $DATABASE;
    my $NODETYPE;
    my $ALL;
    my $rc = 0;

    my $lsxcatd_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "       lsxcatd [-v|--version]";
        push @{ $rsp{data} }, "       lsxcatd [-h|--help]";
        push @{ $rsp{data} }, "       lsxcatd [-d|--database]";
        push @{ $rsp{data} }, "       lsxcatd [-t|--nodetype]";
        push @{ $rsp{data} }, "       lsxcatd [-a|--all]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $cb->(\%rsp);
    };

    # Process arguments
    if ($args) {
        @ARGV = @{$args};
    }
    if (scalar(@ARGV) == 0) { $lsxcatd_usage->(1); return; }
    if (!GetOptions('h|?|help' => \$HELP,
            'v|version'  => \$VERSION,
            'a|all'      => \$ALL,
            't|type'     => \$NODETYPE,
            'd|database' => \$DATABASE))
    { $lsxcatd_usage->(1); return; }

    if ($HELP) { $lsxcatd_usage->(0); return; }

    # Version
    if ($VERSION || $ALL) {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $cb->(\%rsp);
        if (!$ALL) {
            return;
        }
    }

    # nodetype  MN or SN
    if ($NODETYPE || $ALL) {
        my %rsp;
        if (xCAT::Utils->isMN()) {    # if on Management Node
            $rsp{data}->[0] = "This is a Management Node";
            $cb->(\%rsp);
        }
        if (xCAT::Utils->isServiceNode()) {    # if on Management Node
            $rsp{data}->[0] = "This is a Service Node";
            $cb->(\%rsp);
        }
        if (!$ALL) {
            return;
        }
    }

    # no arguments error
    my $xcatcfg;
    my %rsp;
    if ($DATABASE || $ALL) {
        $xcatcfg = xCAT::Table->get_xcatcfg();

        if ($xcatcfg =~ /^SQLite:/) {    # SQLite just return SQlite
            $rsp{data}->[0] = "dbengine=SQLite";
            $cb->(\%rsp);

        }
        if ($xcatcfg =~ /^DB2:/) {       # for DB2 , get schema name
            my @parts = split('\|', $xcatcfg);
            my $cfgloc = $parts[0] . "|" . $parts[1];
            my $instance;
            $instance = $parts[1];
            my ($db2, $databasename) = split(':', $parts[0]);
            my $xcatdbhome = xCAT::Utils->getHomeDir("xcatdb");
            $rsp{data}->[0] = "cfgloc=$cfgloc";
            $rsp{data}->[1] = "dbengine=$db2";
            $rsp{data}->[2] = "dbinstance=$instance";
            $rsp{data}->[3] = "dbname=$databasename";
            $rsp{data}->[4] = "dbloc=$xcatdbhome";

            $cb->(\%rsp);

        }
        if (($xcatcfg =~ /^mysql:/) || ($xcatcfg =~ /^Pg:/)) {
            my @parts = split('\|', $xcatcfg);
            my $cfgloc = $parts[0] . "|" . $parts[1];
            my ($host,   $addr)            = split('host=', $parts[0]);
            my ($engine, $databasenamestr) = split(':',     $host);
            my ($db,     $databasename)    = split('=',     $databasenamestr);
            chop $databasename;

            $rsp{data}->[0] = "cfgloc=$cfgloc";
            $rsp{data}->[1] = "dbengine=$engine";
            $rsp{data}->[2] = "dbname=$databasename";
            $rsp{data}->[3] = "dbhost=$addr";
            $rsp{data}->[4] = "dbadmin=$parts[1]";
            $cb->(\%rsp);
        }
    }

    return $rc;
}

# Prune records from the eventlog or auditlog or all records.
#  Only supports eventlog and auditlog
sub tabprune
{
    my $args = shift;
    my $cb   = shift;
    my $HELP;
    my $VERSION;
    my $ALL;
    my $NUMBERENTRIES;
    my $PERCENT;
    my $VERBOSE;
    my $RECID;
    my $NUMBERDAYS;
    my $rc = 0;

    my $tabprune_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage: tabprune <tablename> [-V] -a";
        push @{ $rsp{data} }, "       tabprune <tablename> [-V] -n <# of records>";
        push @{ $rsp{data} }, "       tabprune <tablename> [-V] -i <recid>";
        push @{ $rsp{data} }, "       tabprune <tablename> [-V] -p <percent>";
        push @{ $rsp{data} }, "       tabprune <tablename> [-V] -d <# of days>";
        push @{ $rsp{data} }, "       tabprune [-h|--help]";
        push @{ $rsp{data} }, "       tabprune [-v|--version]";
        push @{ $rsp{data} }, "       tables supported:eventlog,auditlog,unless -a which supports all tables";
        push @{ $rsp{data} }, "       -d option only supported for eventlog,auditlog";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $cb->(\%rsp);
    };

    # Process arguments
    if ($args) {
        @ARGV = @{$args};
    }
    if (!GetOptions('h|?|help' => \$HELP,
            'v|version'   => \$VERSION,
            'V'           => \$VERBOSE,
            'p|percent=i' => \$PERCENT,
            'd|days=i'    => \$NUMBERDAYS,
            'i|recid=s'   => \$RECID,
            'a'           => \$ALL,
            'n|number=i'  => \$NUMBERENTRIES))
    { $tabprune_usage->(1); return; }

    if ($HELP) { $tabprune_usage->(0); return; }

    # Version
    if ($VERSION) {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $cb->(\%rsp);
        return;
    }
    if (scalar(@ARGV) > 1) { $tabprune_usage->(1); return; }
    my $table = $ARGV[0];
    if (!(defined $table)) {
        my %rsp;
        $rsp{data}->[0] = "Table name required.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;

    }
    $table =~ s/\s*//g;    # remove blanks
    if (($table ne "eventlog") && ($table ne "auditlog") && ($table ne "isnm_perf") && ($table ne "isnm_perf_sum") && (!$ALL)) {
        my %rsp;
        $rsp{data}->[0] = "Table $table not supported, see tabprune -h for supported tables.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;

    }

    # only support days option for eventlog and auditlog
    if (($table ne "eventlog") && ($table ne "auditlog") && (defined $NUMBERDAYS)) {
        my %rsp;
        $rsp{data}->[0] = "Table $table not supported for the -d option.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;

    }
    if ((!(defined $PERCENT)) && (!(defined $NUMBERDAYS)) && (!(defined $RECID)) && (!(defined $ALL)) && (!(defined $NUMBERENTRIES))) {
        my %rsp;
        $rsp{data}->[0] = "One option -p or -i or -n or -a or -d must be supplied.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;

    }
    if ((defined $PERCENT) && ((defined $RECID) || (defined $ALL) || (defined $NUMBERENTRIES))) {
        my %rsp;
        $rsp{data}->[0] = "Only one option -p or -i or -n or -a maybe used at a time.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;

    }
    if ((defined $RECID) && ((defined $PERCENT) || (defined $ALL) || (defined $NUMBERENTRIES))) {
        my %rsp;
        $rsp{data}->[0] = "Only one option -p or -i or -n or -a maybe used at a time.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;
    }
    if ((defined $ALL) && ((defined $PERCENT) || (defined $RECID) || (defined $NUMBERENTRIES))) {
        my %rsp;
        $rsp{data}->[0] = "Only one option -p or -i or -n or -a maybe used at a time.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;
    }
    if ((defined $NUMBERENTRIES) && ((defined $PERCENT) || (defined $RECID) || (defined $ALL))) {
        my %rsp;
        $rsp{data}->[0] = "Only one option -p or -i or -n or -a maybe used at a time.";
        $rsp{errorcode} = 1;
        $cb->(\%rsp);
        return 1;
    }

    # determine the attribute name of the recid
    my $attrrecid;
    if (($table eq "eventlog") || ($table eq "auditlog")) {
        $attrrecid = "recid";
    } else {
        if ($table eq "isnm_perf") { # if ISNM   These tables are really not supported in 2.8 or later
            $attrrecid = "perfid";
        } else {
            $attrrecid = "period";    # isnm_perf_sum table
        }
    }
    if (defined $ALL) {
        $rc = tabprune_all($table, $cb, $attrrecid, $VERBOSE);
    }
    if (defined $NUMBERENTRIES) {
        $rc = tabprune_numberentries($table, $cb, $NUMBERENTRIES, "n", $attrrecid, $VERBOSE);
    }
    if (defined $PERCENT) {
        $rc = tabprune_numberentries($table, $cb, $PERCENT, "p", $attrrecid, $VERBOSE);
    }
    if (defined $RECID) {
        $rc = tabprune_recid($table, $cb, $RECID, $attrrecid, $VERBOSE);
    }
    if (defined $NUMBERDAYS) {
        $rc = tabprune_numberdays($table, $cb, $NUMBERDAYS, $attrrecid, $VERBOSE);
    }
    if (!($VERBOSE)) {    # not putting out changes
        my %rsp;
        push @{ $rsp{data} }, "tabprune of $table complete.";
        $rsp{errorcode} = $rc;
        $cb->(\%rsp);
    }
    return $rc;
}

sub tabprune_all {
    my $table     = shift;
    my $cb        = shift;
    my $attrrecid = shift;
    my $VERBOSE   = shift;
    my $rc        = 0;
    my $tab       = xCAT::Table->new($table);
    unless ($tab) {
        $cb->({ error => "Unable to open  $table", errorcode => 4 });
        return 1;
    }
    if ($VERBOSE) {    # will output change to std
        my $recs = $tab->getAllEntries("all");
        output_table($table, $cb, $tab, $recs);
    }

    $tab->delEntries();    #Yes, delete *all* entries
    $tab->commit;          #  commit
    return $rc;
}

#  prune input number of records for the table
#  if number of entries > number than in the table, then remove all
#  this handles the number of records or percentage to delete
sub tabprune_numberentries {
    my $table         = shift;
    my $cb            = shift;
    my $numberentries = shift;    # either number of entries or percent to
                                  # remove based on the flag
    my $flag          = shift;    # (n or p flag)
    my $attrrecid     = shift;
    my $VERBOSE       = shift;
    my $rc            = 0;
    my $tab = xCAT::Table->new($table);

    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return 1;
    }
    my $RECID;
    my $values = $tab->getMAXMINEntries($attrrecid);
    if ((defined($values->{"max"})) && (defined($values->{"min"}))) {
        my $largerid = $values->{"max"};
        my $smallrid = $values->{"min"};
        if ($flag eq "n") {    # deleting number of records
             #get the smalled recid and add number to delete, that is where to start removing
            $RECID = $smallrid + $numberentries;
        } else {    # flag must be percentage
             #take largest and smallest recid and percentage and determine the recid
             # that will remove the requested percentage.   If some are missing in the
             # middle due to tabedit,  we are not worried about it.

            my $totalnumberrids = $largerid - $smallrid + 1;
            my $percent         = $numberentries / 100;
            my $percentage      = $totalnumberrids * $percent;
            my $cnt = sprintf("%d", $percentage);    # round to whole number
            $RECID = $smallrid + $cnt;    # get recid to remove all before
        }

        # Now prune starting at $RECID
        $rc = tabprune_recid($table, $cb, $RECID, $attrrecid, $VERBOSE);
    } else {
        my %rsp;
        push @{ $rsp{data} }, "Nothing to prune from $table.";
        $rsp{errorcode} = $rc;
        $cb->(\%rsp);
    }
    return $rc;
}

#  prune all entries up to the record id input
#  if rec id  does not exist error
sub tabprune_recid {
    my $table     = shift;
    my $cb        = shift;
    my $recid     = shift;
    my $attrrecid = shift;
    my $VERBOSE   = shift;
    my $rc        = 0;

    # check which database so can build the correct Where clause
    my $tab = xCAT::Table->new($table);
    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return 1;
    }
    my $DBname = xCAT::Utils->get_DBName;

    # display the output
    my @recs;
    if ($VERBOSE) {    # need to get all attributes
        if ($DBname =~ /^DB2/) {
            @recs = $tab->getAllAttribsWhere("\"$attrrecid\"<$recid", 'ALL');
        } else {
            @recs = $tab->getAllAttribsWhere("$attrrecid<$recid", 'ALL');
        }
        output_table($table, $cb, $tab, \@recs);
    }
    my @ents;
    if ($DBname =~ /^DB2/) {
        @ents = $tab->getAllAttribsWhere("\"$attrrecid\"<$recid", $attrrecid);
    } else {
        @ents = $tab->getAllAttribsWhere("$attrrecid<$recid", $attrrecid);
    }

    # delete them
    foreach my $rid (@ents) {
        $tab->delEntries($rid);
    }
    $tab->commit;
    return $rc;
}

#  prune all record up to number of days from today
sub tabprune_numberdays {
    my $table      = shift;
    my $cb         = shift;
    my $numberdays = shift;
    my $attrrecid  = shift;
    my $VERBOSE    = shift;
    my $rc         = 0;

    # check which database so can build the correct Where clause
    my $tab = xCAT::Table->new($table);
    unless ($tab) {
        $cb->({ error => "Unable to open $table", errorcode => 4 });
        return 1;
    }

    # get number of seconds in the day count
    my $numbersecs = ($numberdays * 86400);

    # get time now
    my $timenow     = time;
    my $secsdaysago = $timenow - $numbersecs;

    # Format like the database table timestamp record
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
      localtime($secsdaysago);
    my $daysago = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
        $year + 1900, $mon + 1, $mday,
        $hour, $min, $sec);

    # delete all records before # days ago

    # display the output
    # get field name for the table
    my $timeattr = "audittime";    # default auditlog, most used
    if ($table eq "eventlog") {
        $timeattr = "eventtime";
    }
    my @attrarray;
    push @attrarray, "$timeattr<$daysago";
    my @recs;
    if ($VERBOSE) {                # need to get all attributes
        @recs = $tab->getAllAttribsWhere(\@attrarray, 'ALL');
        output_table($table, $cb, $tab, \@recs);
    }
    my @ents = $tab->getAllAttribsWhere(\@attrarray, $attrrecid);

    # delete them
    foreach my $rid (@ents) {
        $tab->delEntries($rid);
    }
    $tab->commit;
    return $rc;
}


#  Dump table records to  stdout.
sub output_table {
    my $table = shift;
    my $cb    = shift;
    my $tabh  = shift;
    my $recs  = shift;
    my %rsp;
    my $tabdump_header = sub {
        my $header = "#" . join(",", @_);
        push @{ $rsp{data} }, $header;
    };

    # Display all the rows of the table in order of the columns in the schema
    $tabdump_header->(@{ $tabh->{colnames} });
    foreach my $rec (@$recs)
    {
        my $line = '';
        foreach (@{ $tabh->{colnames} })
        {
            if (defined $rec->{$_})
            {
                $rec->{$_} =~ s/"/""/g;
                $line = $line . '"' . $rec->{$_} . '",';
            }
            else
            {
                $line .= ',';
            }
        }
        $line =~ s/,$//;    # remove the extra comma at the end
        push @{ $rsp{data} }, $line;
    }
    $cb->(\%rsp);
    return;
}

sub getTableColumn {
    my $string = shift;
    if ($shortnames{$string}) {
        return @{ $shortnames{$string} };
    }
    unless ($string =~ /\./) {
        return undef;
    }
    return split /\./, $string, 2;
}

#   sub groupch
#   {
#       my $args = shift;
#       my $nodech_usage = sub
#       {
#           my $exitcode = shift @_;
#           my $cmdname = "groupch";
#           my %rsp;
#           push @{$rsp{data}}, "Usage: $cmdname <group1,group2,...> table.column=value [...]";
#           push @{$rsp{data}}, "       $cmdname {-v | --version}";
#           push @{$rsp{data}}, "       $cmdname [-? | -h | --help]";
#           if ($exitcode) { $rsp{errorcode} = $exitcode; }
#           $callback->(\%rsp);
#       };
#       my @args = @{$args};
#       unless (scalar @args >= 2) {
#           $nodech_usage->(1);
#           return;
#       }
#       my @groups = split /,/,shift @args;
#       foreach (@args)
#       {
#   #       if ($deletemode)
#   #       {
#   #           if (m/[=\.]/)   # in delete mode they can only specify tables names
#   #           {
#   #               $callback->({error => [". and = not valid in delete mode."],errorcode=>1});
#   #               next;
#   #           }
#   #           $tables{$_} = 1;
#   #           next;
#   #       }
#           unless (m/=/ or m/!~/)
#           {
#               $callback->({error => ["Malformed argument $_ ignored."],errorcode=>1});
#               next;
#           }
#           my $stable;
#           my $scolumn;
#           #Check for selection criteria
#           if (m/^[^=]*==/) {
#               ($temp,$value)=split /==/,$_,2;
#               ($stable,$scolumn)=getTableColumn($temp);
#               $criteria{$stable}->{$scolumn}=[$value,'match'];

#               next; #Is a selection criteria, not an assignment specification
#           } elsif (m/^[^=]*!=/) {
#               ($temp,$value)=split /!=/,$_,2;
#               ($stable,$scolumn)=getTableColumn($temp);
#               $criteria{$stable}->{$scolumn}=[$value,'natch'];
#               next; #Is a selection criteria, not an assignment specification
#           } elsif (m/^[^=]*=~/) {
#               ($temp,$value)=split /=~/,$_,2;
#               ($stable,$scolumn)=getTableColumn($temp);
#               $value =~ s/^\///;
#               $value =~ s/\/$//;
#               $criteria{$stable}->{$scolumn}=[$value,'regex'];
#               next; #Is a selection criteria, not an assignment specification
#           } elsif (m/^[^=]*!~/) {
#               ($temp,$value)=split /!~/,$_,2;
#               ($stable,$scolumn)=getTableColumn($temp);
#               $value =~ s/^\///;
#               $value =~ s/\/$//;
#               $criteria{$stable}->{$scolumn}=[$value,'negex'];
#               next; #Is a selection criteria, not an assignment specification
#           }
#           #Now definitely an assignment
#
#           ($temp, $value) = split('=', $_, 2);
#           $value =~ s/^@//; #Allow the =@ operator to exist for an unambiguous assignmenet operator
#                             #So before, table.column==value meant set to =value, now it would be matching value
#                             #the new way would be table.column=@=value to be unambiguous
#                             #now a value like '@hi' would be set with table.column=@@hi
#           if ($value eq '') { #If blank, force a null entry to override group settings
#               $value = '|^.*$||';
#           }
#           my $op = '=';
#           if ($temp =~ /,$/)
#           {
#               $op = ',=';
#               chop($temp);
#           }
#           elsif ($temp =~ /\^$/)
#           {
#               $op = '^=';
#               chop($temp);
#           }

#           my $table;
#           if ($shortnames{$temp})
#           {
#               ($table, $column) = @{$shortnames{$temp}};
#           }
#           else
#           {
#               ($table, $column) = split('\.', $temp, 2);
#           }
#           unless (grep /$column/,@{$xCAT::Schema::tabspec{$table}->{cols}}) {
#                $callback->({error=>"$table.$column not a valid table.column description",errorcode=>[1]});
#                return;
#           }

#           # Keep a list of the value/op pairs, in case there is more than 1 per table.column
#           #$tables{$table}->{$column} = [$value, $op];
#           push @{$tables{$table}->{$column}}, ($value, $op);
#       }
#   }


#
#  Process the nodech command, also used by the XML setNodesAttribs utility
#  in tabutils
#
sub nodech
{
    my $nodes    = shift;
    my $args     = shift;
    my $callback = shift;
    my $addmode  = shift;
    my $groupmode;
    if ($addmode eq "groupch") {
        $addmode   = 0;
        $groupmode = 1;
    }
    my $VERSION;
    my $HELP;
    my $deletemode;
    my $grptab;
    my @grplist;

    my $nodech_usage = sub
    {
        my $exitcode  = shift @_;
        my $addmode   = shift @_;
        my $groupmode = shift @_;
        my $cmdname = $addmode ? 'nodeadd' : ($groupmode ? 'nodegrpch' : 'nodech');
        my %rsp;
        if ($addmode) {
            push @{ $rsp{data} }, "Usage: $cmdname <noderange> groups=<groupnames> [table.column=value] [...]";
        } elsif ($groupmode) {
            push @{ $rsp{data} }, "Usage: $cmdname <group1,group2,...> [table.column=value] [...]";
        } else {
            push @{ $rsp{data} }, "Usage: $cmdname <noderange> table.column=value [...]";
            push @{ $rsp{data} }, "       $cmdname {-d | --delete} <noderange> <table> [...]";
        }
        push @{ $rsp{data} }, "       $cmdname {-v | --version}";
        push @{ $rsp{data} }, "       $cmdname [-? | -h | --help]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $callback->(\%rsp);
    };

    if ($args) {
        @ARGV = @{$args};
    } else {
        @ARGV = ();
    }
    my %options = ('h|?|help' => \$HELP, 'v|version' => \$VERSION);
    if (!$addmode) { $options{'d|delete'} = \$deletemode; }
    if (!GetOptions(%options)) {
        $nodech_usage->(1, $addmode, $groupmode);
        return;
    }

    # Help
    if ($HELP) {
        $nodech_usage->(0, $addmode, $groupmode);
        return;
    }

    # Version
    if ($VERSION) {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $callback->(\%rsp);
        return;
    }

    # Note: the noderange comes through in $arg (and therefore @ARGV) for nodeadd,
    # because it is linked to xcatclientnnr, since the nodes specified in the noderange
    # do not exist yet.  The nodech cmd is linked to xcatclient, so its noderange is
    # put in $nodes instead of $args.
    if (scalar(@ARGV) < (1 + $addmode)) { $nodech_usage->(1, $addmode); return; }
    my @groups;

    if ($addmode)
    {
        my $nr = shift @ARGV;
        $nodes = [ noderange($nr, 0) ];
        unless ($nodes) {
            $callback->({ error => "No noderange to add.\n", errorcode => 1 });
            return;
        }

        my $invalidnodename = ();
        foreach my $node (@$nodes) {
            if ($node =~ /[A-Z]/) {
                $invalidnodename .= ",$node";
            }
        }
        if ($invalidnodename) {
            $invalidnodename =~ s/,//;
            $callback->({ warning => "The node name \'$invalidnodename\' contains capital letters which may not be resolved correctly by the dns server." });
        }
    } elsif ($groupmode) {
        @groups = split /,/, shift @ARGV;
    }
    my $column;
    my $value;
    my $temp;
    my %tables;
    my %criteria = ();
    my $tab;

    #print Dumper($deletemode);
    foreach (@ARGV)
    {
        if ($deletemode)
        {
            if (m/[=\.]/)    # in delete mode they can only specify tables names
            {
                $callback->({ error => [". and = not valid in delete mode."], errorcode => 1 });
                next;
            }
            $tables{$_} = 1;
            next;
        }
        unless (m/=/ or m/!~/)
        {
            $callback->({ error => ["Malformed argument $_ ignored."], errorcode => 1 });
            next;
        }
        my $stable;
        my $scolumn;

        #Check for selection criteria
        if (m/^[^=]*==/) {
            ($temp, $value) = split /==/, $_, 2;
            ($stable, $scolumn) = getTableColumn($temp);
            $criteria{$stable}->{$scolumn} = [ $value, 'match' ];

            next;    #Is a selection criteria, not an assignment specification
        } elsif (m/^[^=]*!=/) {
            ($temp, $value) = split /!=/, $_, 2;
            ($stable, $scolumn) = getTableColumn($temp);
            $criteria{$stable}->{$scolumn} = [ $value, 'natch' ];
            next;    #Is a selection criteria, not an assignment specification
        } elsif (m/^[^=]*=~/) {
            ($temp, $value) = split /=~/, $_, 2;
            ($stable, $scolumn) = getTableColumn($temp);
            $value =~ s/^\///;
            $value =~ s/\/$//;
            $criteria{$stable}->{$scolumn} = [ $value, 'regex' ];
            next;    #Is a selection criteria, not an assignment specification
        } elsif (m/^[^=]*!~/) {
            ($temp, $value) = split /!~/, $_, 2;
            ($stable, $scolumn) = getTableColumn($temp);
            $value =~ s/^\///;
            $value =~ s/\/$//;
            $criteria{$stable}->{$scolumn} = [ $value, 'negex' ];
            next;    #Is a selection criteria, not an assignment specification
        }

        #Now definitely an assignment

        ($temp, $value) = split('=', $_, 2);
        $value =~ s/^@//; #Allow the =@ operator to exist for an unambiguous assignmenet operator
         #So before, table.column==value meant set to =value, now it would be matching value
         #the new way would be table.column=@=value to be unambiguous
         #now a value like '@hi' would be set with table.column=@@hi
        if ($value eq '') { #If blank, force a null entry to override group settings
            $value = '|^.*$||';
        }
        my $op = '=';
        if ($temp =~ /,$/)
        {
            $op = ',=';
            chop($temp);
        }
        elsif ($temp =~ /\^$/)
        {
            $op = '^=';
            chop($temp);
        }

        my $table;
        if ($shortnames{$temp})
        {
            ($table, $column) = @{ $shortnames{$temp} };
        }
        else
        {
            ($table, $column) = split('\.', $temp, 2);
        }
        unless (grep /$column/, @{ $xCAT::Schema::tabspec{$table}->{cols} }) {
            $callback->({ error => "$table.$column not a valid table.column description", errorcode => [1] });
            return;
        }

        # Keep a list of the value/op pairs, in case there is more than 1 per table.column
        #$tables{$table}->{$column} = [$value, $op];
        push @{ $tables{$table}->{$column} }, ($value, $op);
    }
    my %nodehash;
    if (keys %criteria) {
        foreach (@$nodes) {
            $nodehash{$_} = 1;
        }
    }
    foreach $tab (keys %criteria) {
        my $tabhdl  = xCAT::Table->new($tab, -create => 1, -autocommit => 0);
        my @columns = keys %{ $criteria{$tab} };
        my $tabhash = $tabhdl->getNodesAttribs($nodes, \@columns);
        my $node;
        my $col;
        my $rec;
        foreach $node (@$nodes) {
            foreach $rec (@{ $tabhash->{$node} }) {
                foreach $col (@columns) {
                    my $value = $criteria{$tab}->{$col}->[0];
                    unless (defined $value) {
                        $value = "";
                    }
                    my $matchtype = $criteria{$tab}->{$col}->[1];
                    if ($matchtype eq 'match' and not($rec->{$col} eq $value) or
                        $matchtype eq 'natch' and ($rec->{$col} eq $value)   or
                        $matchtype eq 'regex' and ($rec->{$col} !~ /$value/) or
                        $matchtype eq 'negex' and ($rec->{$col} =~ /$value/)) {
                        delete $nodehash{$node};
                    }
                }
            }
        }
        $nodes = [ keys %nodehash ];
    }
    foreach $tab (keys %tables)
    {
        my $tabhdl = xCAT::Table->new($tab, -create => 1, -autocommit => 0);
        if ($tabhdl)
        {
            my $changed = 0;
            my @entities;
            if ($groupmode) {
                @entities = @groups;
            } else {
                @entities = @$nodes;
            }
            my $entity;
            foreach $entity (@entities) {
                if ($deletemode) {
                    $tabhdl->delEntries({ 'node' => $entity });
                    $changed = 1;
                } else {

                    #$tabhdl->setNodeAttribs($_,$tables{$tab});
                    my %uhsh;
                    foreach (keys %{ $tables{$tab} }) # for each column specified for this table
                    {
                        #my $op  = $tables{$tab}->{$_}->[1];
                        #my $val = $tables{$tab}->{$_}->[0];
                        my @valoppairs = @{ $tables{$tab}->{$_} };    #Deep copy
                        while (scalar(@valoppairs)) { # alternating list of value and op for this table.column
                            my $val = shift @valoppairs;
                            my $op  = shift @valoppairs;
                            my $key = $_;

                            # When changing the groups of the node, check whether the new group
                            # is a dynamic group.
                            if (($key eq 'groups') && ($op eq '=')) {
                                if ($groupmode) {
                                    $callback->({ error => "Group membership is not changeable via nodegrpch", errorcode => 1 });
                                    return;
                                }
                                if (scalar(@grplist) == 0) { # Do not call $grptab->getAllEntries for each node, performance issue.
                                    $grptab = xCAT::Table->new('nodegroup');
                                    if ($grptab) {
                                        @grplist = @{ $grptab->getAllEntries() };
                                    }
                                }
                                my @grps = split(/,/, $val);
                                foreach my $grp (@grps) {
                                    foreach my $grpdef_ref (@grplist) {
                                        my %grpdef = %$grpdef_ref;
                                        if (($grpdef{'groupname'} eq $grp) && ($grpdef{'grouptype'} eq 'dynamic')) {
                                            my %rsp;
                                            $rsp{data}->[0] = "nodegroup $grp is a dynamic node group, should not add a node into a dynamic node group statically.\n";
                                            $callback->(\%rsp);
                                        }
                                    }
                                }
                            }
                            if ($op eq '=') {
                                $uhsh{$key} = $val;
                            } elsif ($op eq ',=') {    #splice assignment
                                if ($groupmode) {
                                    $callback->({ error => ",= and ^= are not supported by nodegrpch", errorcode => 1 });
                                    return;
                                }
                                my $curval = $uhsh{$key}; # in case it was already set
                                if (!defined($curval)) {
                                    my $cent = $tabhdl->getNodeAttribs($entity, [$key]);
                                    if ($cent) { $curval = $cent->{$key}; }
                                }
                                if ($curval) {
                                    my @vals = split(/,/, $curval);
                                    unless (grep /^$val$/, @vals) {
                                        unshift @vals, $val;
                                        my $newval = join(',', @vals);
                                        $uhsh{$key} = $newval;
                                    }
                                } else {
                                    $uhsh{$key} = $val;
                                }
                            } elsif ($op eq '^=') {
                                if ($groupmode) {
                                    $callback->({ error => ",= and ^= are not supported by nodegrpch", errorcode => 1 });
                                    return;
                                }
                                my $curval = $uhsh{$key}; # in case it was already set
                                if (!defined($curval)) {
                                    my $cent = $tabhdl->getNodeAttribs($entity, [$key]);
                                    if ($cent) { $curval = $cent->{$key}; }
                                }
                                if ($curval) {
                                    my @vals = split(/,/, $curval);
                                    if (grep /^$val$/, @vals) { #only bother if there
                                        @vals = grep(!/^$val$/, @vals);
                                        my $newval = join(',', @vals);
                                        $uhsh{$key} = $newval;
                                    }
                                }  #else, what they asked for is the case alredy
                            }
                        }    # end of while @valoppairs
                    }    # end of foreach column specified for this table

                    if (keys %uhsh)
                    {
                        if ($groupmode) {
                            my $nodekey = "node";
                            if (defined $xCAT::Schema::tabspec{$tab}->{nodecol}) {
                                $nodekey = $xCAT::Schema::tabspec{$tab}->{nodecol}
                            }
                            my %clrhash; #First, we prepare to clear all nodes of their overrides on these columns
                            foreach (keys %uhsh) {
                                if ($_ eq $nodekey) { next; } #skip attempts to manipulate 'node' type columns in a groupch
                                $clrhash{$_} = "";
                            }
                            $tabhdl->setAttribs({ $nodekey => $entity }, \%uhsh);
                            $changed = 1;
                            $nodes   = [ noderange($entity) ];
                            unless (scalar @$nodes) { next; }
                            $tabhdl->setNodesAttribs($nodes, \%clrhash);
                            $changed = 1;
                        } else {
                            my @rc = $tabhdl->setNodeAttribs($entity, \%uhsh);
                            $changed = 1;
                            if (not defined($rc[0])) {
                                $callback->({ error => "DB error " . $rc[1], errorcode => 1 });
                            }
                        }
                    }
                }
            }
            if ($changed) {
                $tabhdl->commit;
            }
        }
        else
        {
            $callback->(
                { error => ["ERROR: Unable to open table $tab in configuration"], errorcode => 1 }
            );
        }
    }
}

# gennr linked to xcatclientnnr and is used to generate a list of nodes
# external to the database.
sub gennr {
    my $nodes    = shift;
    my $args     = shift;
    my $callback = shift;
    @ARGV = @{$args};
    my $nr = shift @ARGV;
    $nodes = [ noderange($nr, 0) ];
    my %rsp;    # for output.
    foreach (@$nodes) {

        #print $_ . "\n";
        push @{ $rsp{data} }, $_;

    }
    $callback->(\%rsp);
}

sub tabgrep
{
    my $node = shift;
    my @tablist;
    my $callback = shift;

    if (!defined($node) || !scalar(@$node)) {
        my %rsp;
        push @{ $rsp{data} }, "Usage: tabgrep nodename";
        push @{ $rsp{data} }, "       tabgrep [-?|-h|--help]";
        $rsp{errorcode} = 1;
        $callback->(\%rsp);
        return;
    }

    foreach (keys %{xCAT::Schema::tabspec})
    {
        if (grep /^node$/, @{ $xCAT::Schema::tabspec{$_}->{cols} })
        {
            push @tablist, $_;
        }
    }
    foreach (@tablist)
    {
        my $tab = xCAT::Table->new($_);
        unless ($tab) { next; }
        if ($tab and $tab->getNodeAttribs($node->[0], ["node"]))
        {
            $callback->({ data => [$_] });
        }
        $tab->close;
    }

}

sub rnoderange
{
    my $nodes    = shift;
    my $args     = shift;
    my $callback = shift;
    my $data     = abbreviate_noderange($nodes);
    if ($data) {
        $callback->({ data => [$data] });
    }
}
#####################################################
#  nodels command
#####################################################
sub nodels
{
    my $nodes     = shift;
    my $args      = shift;
    my $callback  = shift;
    my $noderange = shift;
    unless ($nodes) {
        $nodes = [];
    }

    my $VERSION;
    my $HELP;

    my $nodels_usage = sub
    {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage:";
        push @{ $rsp{data} }, "  nodels [noderange] [-b|--blame] [-H|--with-fieldname] [table.attribute | shortname] [-S][...]";
        push @{ $rsp{data} }, "  nodels {-v|--version}";
        push @{ $rsp{data} }, "  nodels [-?|-h|--help]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $callback->(\%rsp);
    };

    if ($args) {
        @ARGV = @{$args};
    } else {
        @ARGV = ();
    }
    my $NOTERSE;
    my $ATTRIBUTION;
    my $HIDDEN;

    if (!GetOptions('h|?|help' => \$HELP, 'H|with-fieldname' => \$NOTERSE, 'b|blame' => \$ATTRIBUTION, 'v|version' => \$VERSION, 'S' => \$HIDDEN)) { $nodels_usage->(1); return; }

    # Help
    if ($HELP) { $nodels_usage->(0); return; }

    # Version
    if ($VERSION)
    {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "$version";
        $callback->(\%rsp);
        return;
    }

    # TODO -- Parse command arguments
    #  my $opt;
    #  my %attrs;
    #  foreach $opt (@ARGV) {
    #     if ($opt =~ /^group/) {
    #     }
    #  }
    my $argc  = @ARGV;
    my $terse = 2;
    if ($NOTERSE) {
        $terse = 0;
    }

    if (@$nodes > 0 or $noderange)
    { #Make sure that there are zero nodes *and* that a noderange wasn't requested
                    # TODO - gather data for each node
                    #        for now just return the flattened list of nodes)
        my $rsp;    #build up fewer requests, be less chatty

        #-S will make nodels not show FSPs and BPAs
        my @newnodes = ();
        if (!defined($HIDDEN))
        {
            my $listtab = xCAT::Table->new('nodelist');
            if ($listtab) {
                my $listHash = $listtab->getNodesAttribs(\@$nodes, ['hidden']);
                foreach my $rnode (@$nodes) {
                    unless (defined($listHash->{$rnode}->[0]->{hidden})) {
                        push(@newnodes, $rnode);
                    } elsif ($listHash->{$rnode}->[0]->{hidden} != 1) {
                        push(@newnodes, $rnode);
                    }
                }
            }
            $nodes = \@newnodes;
        }

        if ($argc)
        {
            my %tables;
            foreach (@ARGV)
            {
                my $table;
                my $column;
                my $value;
                my $matchtype;
                my $temp = $_;
                if ($temp =~ /^[^=]*\!=/) {
                    ($temp, $value) = split /!=/, $temp, 2;
                    $matchtype = 'natch';
                }
                elsif ($temp =~ /^[^=]*=~/) {
                    ($temp, $value) = split /=~/, $temp, 2;
                    $value =~ s/^\///;
                    $value =~ s/\/$//;
                    $matchtype = 'regex';
                }
                elsif ($temp =~ /[^=]*==/) {
                    ($temp, $value) = split /==/, $temp, 2;
                    $matchtype = 'match';
                }
                elsif ($temp =~ /[^=]*!~/) {
                    ($temp, $value) = split /!~/, $temp, 2;
                    $value =~ s/^\///;
                    $value =~ s/\/$//;
                    $matchtype = 'negex';
                }
                if ($shortnames{$temp})
                {
                    ($table, $column) = @{ $shortnames{$temp} };
                    $terse--;
                } elsif ($temp =~ /\./) {
                    ($table, $column) = split('\.', $temp, 2);
                    $terse--;
                } elsif ($xCAT::Schema::tabspec{$temp}) {
                    $terse = 0;
                    $table = $temp;
                    foreach my $column (@{ $xCAT::Schema::tabspec{$table}->{cols} }) {
                        unless (grep /^$column$/, @{ $tables{$table} }) {
                            push @{ $tables{$table} }, [ $column, "$temp.$column" ];
                        }
                    }
                    next;
                } else {
                    $callback->({ error => "$temp not a valid table.column description", errorcode => [1] });
                    next;
                }


                unless (grep /$column/, @{ $xCAT::Schema::tabspec{$table}->{cols} }) {
                    $callback->({ error => "$table.$column not a valid table.column description", errorcode => [1] });
                    next;
                }
                unless (grep /^$column$/, @{ $tables{$table} })
                {
                    push @{ $tables{$table} },
                      [ $column, $temp, $value, $matchtype ]; #Mark this as something to get
                }
            }
            my $tab;
            my %noderecs;
            my %filterednodes    = ();
            my %mustdisplaynodes = ();
            my %forcedisplaykeys = ();
            foreach $tab (keys %tables)
            {
                my $tabh = xCAT::Table->new($tab);
                unless ($tabh) { next; }

                #print Dumper($tables{$tab});
                my $node;
                my %labels;
                my %values;
                my %matchtypes;
                my @cols = ();
                foreach (@{ $tables{$tab} })
                {
                    push @cols, $_->[0];
                    $labels{ $_->[0] } = $_->[1]; #Remember user supplied discreptions and use them
                    if (not defined $values{ $_->[0] }) { #If selection criteria not previously specified
                        $values{ $_->[0] } = $_->[2]; #assign selection criteria
                    } elsif (not defined $_->[2]) { #we already have selection criteria, but this field isn't that
                        $forcedisplaykeys{ $_->[0] } = 1; #allow switch.switch=~switch switch.switch, for example
                    } else { #User attempted multiple selection criteria on the same field, bail
                        $callback->({ error => [ "Multiple selection critera for " . $labels{ $_->[0] } ] });
                        return;
                    }
                    if (not defined $matchtypes{ $_->[0] }) {
                        $matchtypes{ $_->[0] } = $_->[3];
                    }
                }
                my $nodekey = "node";
                if (defined $xCAT::Schema::tabspec{$tab}->{nodecol}) {
                    $nodekey = $xCAT::Schema::tabspec{$tab}->{nodecol}
                }

                my $removenodecol = 1;
                if (grep /^$nodekey$/, @cols) {
                    $removenodecol = 0;
                }
                my $rechash = $tabh->getNodesAttribs($nodes, \@cols, withattribution => $ATTRIBUTION);
                foreach $node (@$nodes)
                {
                    my @cols;
                    my $recs = $rechash->{$node}; #$tabh->getNodeAttribs($node, \@cols);
                    my %satisfiedreqs = ();
                    foreach my $rec (@$recs) {

                        foreach (keys %$rec)
                        {
                            if ($_ eq '!!xcatgroupattribution!!') { next; }
                            if ($_ eq $nodekey and $removenodecol) { next; }
                            $satisfiedreqs{$_} = 1;
                            my %datseg = ();
                            if (defined $values{$_}) {
                                my $criteria = $values{$_}; #At least vim highlighting makes me worry about syntax in regex
                                if ($matchtypes{$_} eq 'match' and not($rec->{$_} eq $criteria) or
                                    $matchtypes{$_} eq 'natch' and ($rec->{$_} eq $criteria) or
                                    $matchtypes{$_} eq 'regex' and ($rec->{$_} !~ /$criteria/) or
                                    $matchtypes{$_} eq 'negex' and ($rec->{$_} =~ /$criteria/)) {

                                    #unless ($rec->{$_} eq $values{$_}) {
                                    $filterednodes{$node} = 1;
                                    next;
                                }
                                $mustdisplaynodes{$node} = 1;
                                unless ($forcedisplaykeys{$_}) { next; } #skip if only specified once on command line
                            }
                            unless ($terse > 0) {
                                $datseg{data}->[0]->{desc} = [ $labels{$_} ];
                            }
                            if ($rec->{'!!xcatgroupattribution!!'} and $rec->{'!!xcatgroupattribution!!'}->{$_}) {
                                $datseg{data}->[0]->{contents} = [ $rec->{$_} . " (inherited from group " . $rec->{'!!xcatgroupattribution!!'}->{$_} . ")" ];
                            } else {
                                $datseg{data}->[0]->{contents} = [ $rec->{$_} ];
                            }
                            $datseg{name} = [$node]; #{}->{contents} = [$rec->{$_}];
                            push @{ $noderecs{$node} }, \%datseg;
                        }
                    }
                    foreach (keys %labels) {
                        unless (defined $satisfiedreqs{$_}) {
                            my %dataseg;
                            if (defined $values{$_}) {
                                my $criteria = $values{$_};
                                if ($matchtypes{$_} eq 'match' and not("" eq $criteria) or
                                    $matchtypes{$_} eq 'natch' and ("" eq $criteria) or
                                    $matchtypes{$_} eq 'regex' and ("" !~ /$criteria/) or
                                    $matchtypes{$_} eq 'negex' and ("" =~ /$criteria/)) {

                                    #unless ("" eq $values{$_}) {
                                    $filterednodes{$node} = 1;
                                    next;
                                }
                                $mustdisplaynodes{$node} = 1;
                                unless ($forcedisplaykeys{$_}) { next; }
                            }
                            $dataseg{name} = [$node];
                            unless ($terse > 0) {
                                $dataseg{data}->[0]->{desc} = [ $labels{$_} ];
                            }
                            $dataseg{data}->[0]->{contents} = [""];
                            push @{ $noderecs{$node} }, \%dataseg;
                        }
                    }
                }

                #$rsp->{node}->[0]->{data}->[0]->{desc}->[0] = $_;
                #$rsp->{node}->[0]->{data}->[0]->{contents}->[0] = $_;
                $tabh->close();
                undef $tabh;
            }
            foreach (keys %mustdisplaynodes) {
                if ($filterednodes{$_} or defined $noderecs{$_}) {
                    next;
                }
                $noderecs{$_} = [ { name => [$_] } ];
            }
            foreach (keys %filterednodes) {
                delete $noderecs{$_};
            }
            foreach (sort (keys %noderecs))
            {
                push @{ $rsp->{"node"} }, @{ $noderecs{$_} };
            }
        }
        else
        {
            foreach (sort @$nodes)
            {
                my $noderec;
                $noderec->{name}->[0] = ($_);
                push @{ $rsp->{node} }, $noderec;
            }
        }
        $callback->($rsp);
    }
    else
    {

        # no noderange specified on command line, return list of all nodes
        my $nodelisttab;
        if ($nodelisttab = xCAT::Table->new("nodelist"))
        {
            my @attribs = ("node");
            my @ents    = $nodelisttab->getAllAttribs(@attribs);
            my @nodes;
            foreach (@ents) {
                if ($_->{node}) {
                    push @nodes, $_->{node};
                }
            }

            #-S will make nodels not show FSPs and BPAs
            my @newnodes = ();
            if (!defined($HIDDEN))
            {
                my $listtab = xCAT::Table->new('nodelist');
                if ($listtab) {
                    my $listHash = $listtab->getNodesAttribs(\@nodes, ['hidden']);
                    foreach my $rnode (@nodes) {
                        unless (defined($listHash->{$rnode}->[0]->{hidden})) {
                            push(@newnodes, $rnode);
                        } elsif ($listHash->{$rnode}->[0]->{hidden} != 1) {
                            push(@newnodes, $rnode);
                        }
                    }
                }
                @nodes = ();
                foreach (@newnodes) {
                    push(@nodes, $_);
                }
            }
            @nodes = sort { $a cmp $b } @nodes;
            foreach (@nodes) {
                my $rsp;

                #if ($_)
                #{
                $rsp->{node}->[0]->{name}->[0] = ($_);

                #              $rsp->{node}->[0]->{data}->[0]->{contents}->[0]="$_->{node} node contents";
                #              $rsp->{node}->[0]->{data}->[0]->{desc}->[0]="$_->{node} node desc";
                $callback->($rsp);

                #}
            }
        }
    }

    return 0;
}

#########
#  tabch
#########

sub tabch {
    my $req      = shift;
    my $callback = shift;

    # tabch usages message
    my $tabch_usage = sub {
        my $exitcode = shift @_;
        my %rsp;
        push @{ $rsp{data} }, "Usage: tabch";
        push @{ $rsp{data} }, "       To add or update rows for tables:";
        push @{ $rsp{data} }, "       tabch [keycolname=keyvalue[,keycolname=keyvalue...]] [tablename.colname=newvalue] [tablename.colname=newvalue]...";
        push @{ $rsp{data} }, "       To delete rows from tables:";
        push @{ $rsp{data} }, "       tabch -d|--delete keycolname=keyvalue[,keycolname=keyvalue...] tablename [tablename]...";
        push @{ $rsp{data} }, "         keycolname=keyvalue   a column name-and-value pair ";
        push @{ $rsp{data} }, "         that identifies the rows in a table to be changed.";
        push @{ $rsp{data} }, "         a column name-and-value pair that identifies ";
        push @{ $rsp{data} }, "         the rows in a table to be changed.";
        push @{ $rsp{data} }, "         that identifies the rows in a table to be changed.";
        push @{ $rsp{data} }, "         tablename.colname=newvalue ";
        push @{ $rsp{data} }, "         the new value for the specified row and column of the table.";
        push @{ $rsp{data} }, "       tabch [-h|--help]";
        push @{ $rsp{data} }, "       tabch [-v|--version]";
        if ($exitcode) { $rsp{errorcode} = $exitcode; }
        $callback->(\%rsp);
    };

    # check for parameters
    if (!defined($req->{arg})) { $tabch_usage->(1); return; }
    @ARGV = @{ $req->{arg} };

    # options can be bundled up like -vV
    Getopt::Long::Configure("bundling");
    $Getopt::Long::ignorecase = 0;
    my $delete;
    my $help;
    my $version;

    # parse the options
    if (
        !GetOptions(
            'd|delete'  => \$delete,
            'h|help'    => \$help,
            'v|version' => \$version,
        )
      )
    {
        $tabch_usage->(1);
        return;
    }

    if ($help) { $tabch_usage->(0); return; }

    # display the version statement if -v or --verison is specified
    if ($version)
    {
        my %rsp;
        my $version = xCAT::Utils->Version();
        $rsp{data}->[0] = "tabch :$version";
        $callback->(\%rsp);
        exit(0);
    }

    # now start processing the input

    my $target = shift @ARGV;
    unless ($target)
    {

        $tabch_usage->(1); return;
    }
    my %tables;
    my %keyhash = ();
    my @keypairs = split(/,/, $target);
    if ($keypairs[0] !~ /([^\.\=]+)\.([^\.\=]+)\=(.+)/)
    {
        foreach (@keypairs)
        {
            m/(.*)=(.*)/;
            my $key = $1;
            my $val = $2;
            if (!defined($key) || !defined($val))
            {
                my %rsp;
                $rsp{data}->[0] = "Incorrect argument \"$_\".\n";
                $rsp{data}->[1] = "Check man tabch or tabch -h\n";
                $callback->(\%rsp);
                return 1;
            }
            $keyhash{$key} = $val;
        }
    }
    else
    {
        unshift(@ARGV, $target);
    }

    if ($delete)
    {

        #delete option is specified
        my @tables_to_del = @ARGV;
        if (@tables_to_del == 0)
        {
            my %rsp;
            $rsp{data}->[0] = "Missing table name.\n";
            $rsp{data}->[1] = "Check man tabch or tabch -h\n";
            $callback->(\%rsp);
            return 1;
        }
        for (@tables_to_del)
        {
            $tables{$_} = xCAT::Table->new($_, -create => 1, -autocommit => 0);
            $tables{$_}->delEntries(\%keyhash);
            $tables{$_}->commit;
        }
    }
    else {
        #update or create option
        my %tableupdates;
        for (@ARGV) {
            my $temp;
            my $table;
            my $column;
            my $value;

            ($table, $temp) = split('\.', $_, 2);

            #try to create the entry if it doesn't exist
            unless ($tables{$table}) {
                my $tab = xCAT::Table->new($table, -create => 1, -autocommit => 0);
                if ($tab) {
                    $tables{$table} = $tab;
                } else {
                    my %rsp;
                    $rsp{data}->[0] = "Table $table does not exist.\n";
                    $callback->(\%rsp);
                    return 1;

                }
            }

            #splice assignment
            if (grep /\+=/, $temp) {
                ($column, $value) = split('\+=', $temp, 2);

                #grab the current values to check against
                my ($attrHash) = $tables{$table}->getAttribs(\%keyhash, $column);
                my @existing = split(",", $attrHash->{$column});

                #if it has values, merge the new and old ones together so no dupes
                if (@existing) {
                    my @values = split(",", $value);
                    my %seen   = ();
                    my @uniq   = ();
                    my $item;

                    foreach $item (@existing, @values) {
                        unless ($seen{$item}) {

                            # if we get here, we have not seen it before
                            $seen{$item} = 1;
                            push(@uniq, $item);
                        }
                    }
                    $value = join(",", @uniq);
                }
            }

            #normal non-splicing assignment
            else {
                ($column, $value) = split("=", $temp, 2);
            }
            unless (grep /$column/, @{ $xCAT::Schema::tabspec{$table}->{cols} }) {
                $callback->({ error => "$table.$column not a valid table.column description", errorcode => [1] });
                return;
            }
            $tableupdates{$table}{$column} = $value;
        }

        #commit all the changes
        my $rollback;
        foreach (keys %tables) {
            if (exists($tableupdates{$_})) {
                my @rc = $tables{$_}->setAttribs(\%keyhash, \%{ $tableupdates{$_} });
                if (not defined($rc[0]))
                {
                    $rollback = 1;
                    $callback->({ error => "DB error " . $rc[1], errorcode => [4] });
                }
            }
            if ($rollback)
            {
                $tables{$_}->rollback();
                $tables{$_}->close;
                undef $tables{$_};
                return;
            }
            else
            {
                $tables{$_}->commit;
            }
        }
    }
}


#
# getAllEntries
#
# Read all the rows from the input table name and returns the response, so
# that the XML will look like this
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getAllEntries</command>
#<table>nodelist</table>
#</xcatrequest>


#<xcatresponse>
#<row>
#<attr1>value1</attr1>
#.
#.
#.
#<attrN>valueN</attrN>
#</row>
#.
#.
#.
#</xcatresponse>
#
#
sub getAllEntries
{
    my $request   = shift;
    my $cb        = shift;
    my $command   = $request->{command}->[0];
    my $tablename = $request->{table}->[0];
    my $tab       = xCAT::Table->new($tablename);
    my %rsp;
    my $recs = $tab->getAllEntries("all");
    unless (@$recs)    # table exists, but is empty.  Show header.
    {

        if (defined($xCAT::Schema::tabspec{$tablename}))
        {
            my $header = "#";
            my @array  = @{ $xCAT::Schema::tabspec{$tablename}->{cols} };
            foreach my $arow (@array) {
                $header .= $arow;
                $header .= ",";
            }
            chop $header;
            push @{ $rsp{row} }, $header;
            $cb->(\%rsp);
            return;
        }
    }
    my %noderecs;
    foreach my $rec (@$recs) {
        my %datseg = ();
        foreach my $key (keys %$rec) {
            $datseg{$key} = $rec->{$key};
        }
        push @{ $noderecs{"row"} }, \%datseg;
    }
    push @{ $rsp{"row"} }, @{ $noderecs{"row"} };

    # for checkin XML created
    #my  $xmlrec=XMLout(\%rsp,RootName=>'xcatresponse',NoAttr=>1,KeyAttr=>[]);
    $cb->(\%rsp);

    return;
}

# getNodesAttribs
# Read the array of  attributes for the noderange  from the input table.
# If the <attr>ALL</attr> is input then read all the attributes
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getNodesAttribs</command>
#<table>nodelist</table>
#<noderange>blade01-blade02</noderange>
#<attr>groups</attr>
#<attr>status</attr>
#</xcatrequest>
#
#<xcatresponse>
#<node>
#<name>nodename</name>
#<attr1>value1</attr1>
#.
#.
#.
#<attrN>valueN</attrN>
#</node>
#.
#.
#.
#</xcatresponse>
#
#
sub getNodesAttribs
{
    my $request   = shift;
    my $cb        = shift;
    my $node      = $request->{node};
    my $command   = $request->{command}->[0];
    my $tablename = $request->{table}->[0];
    my $attr      = $request->{attr};
    my $tab       = xCAT::Table->new($tablename);
    my @nodes     = @$node;
    my @attrs     = @$attr;
    my %rsp;
    my %noderecs;

    if (grep (/ALL/, @attrs)) {  # read the  schema and build array of all attrs
        @attrs = ();
        my $schema = xCAT::Table->getTableSchema($tablename);
        my $desc   = $schema->{descriptions};
        foreach my $c (@{ $schema->{cols} }) {

            # my $space = (length($c)<7 ? "\t\t" : "\t");
            push @attrs, $c;
        }
    }
    my $rechash = $tab->getNodesAttribs(\@nodes, \@attrs);
    foreach my $node (@nodes) {
        my $recs = $rechash->{$node};
        foreach my $rec (@$recs) {
            my %datseg = ();
            $datseg{name} = [$node];
            foreach my $key (keys %$rec) {
                if ($key ne "node") {   # do not put in the added node attribute
                    $datseg{$key} = [ $rec->{$key} ];
                }
            }
            push @{ $noderecs{$node} }, \%datseg;
        }
        push @{ $rsp{"node"} }, @{ $noderecs{$node} };

    }

    # for checkin XML created
    #my  $xmlrec=XMLout(\%rsp,RootName=>'xcatresponse',NoAttr=>1,KeyAttr=>[]);
    $cb->(\%rsp);
    return;
}

# getTablesAllNodeAttribs
# Read  all the nodes from the input tables and get the input attributes
# or get ALL attributes, if the word ALL is used.
# If the <attr>ALL</attr> is input then read all the attributes
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getTablesAllNodeAttribs</command>
#<table>
#<tablename>nodelist</tablename>
#<attr>groups</attr>
#<attr>status</attr>
#</table>
#<table>
#<tablename>nodetype</tablename>
#<attr>ALL</attr>
#</table>
#   .
#   .
#   .
#</xcatrequest>
#
#<xcatresponse>
#<table>
#<tablename>tablename1</tablename>
#<node>
#<name>n1</name>
#<attr1>value1</attr1>
#<attr2>value1</attr2>
#.
#<attrN>valueN</attrN>
#</node>
#</table>
#   .
#   .
#   .
#</xcatresponse>
#
sub getTablesAllNodeAttribs
{
    my $request = shift;
    my $cb      = shift;
    my $command = $request->{command}->[0];
    my %rsp;

    # process each table in the request
    my $tables = $request->{table};
    foreach my $tabhash (@$tables) {

        my $tablename = $tabhash->{tablename}->[0];
        my $attr      = $tabhash->{attr};
        my @attrs     = @$attr;
        my $tab       = xCAT::Table->new($tablename);
        my %noderecs;
        my $recs;

        # build the table name record
        @{ $noderecs{table}->[0]->{tablename} } = $tablename;

        # if request for ALL attributes
        if (grep (/ALL/, @attrs)) { # read the  schema and build array of all attrs
            @attrs = ();
            my $schema = xCAT::Table->getTableSchema($tablename);
            my $desc   = $schema->{descriptions};
            foreach my $c (@{ $schema->{cols} }) {

                # my $space = (length($c)<7 ? "\t\t" : "\t");
                push @attrs, $c;
            }
        }

        # read all the nodes and their attributes in this table
        my @nodeentries = $tab->getAllNodeAttribs(\@attrs);
        foreach my $node (@nodeentries) {

            # build the node entrys
            my %datseg = ();
            $datseg{name} = $node->{node};
            foreach my $at (@attrs) {

                # if the attribute has a value and is not the node attribute
                if (($node->{$at}) && ($at ne "node")) {
                    $datseg{$at} = $node->{$at};
                }
            }
            push @{ $noderecs{table}->[0]->{node} }, \%datseg;
        }
        push @{ $rsp{"table"} }, @{ $noderecs{table} };
    }    # end of all table processing

    # for checkin XML created
    #my  $xmlrec=XMLout(\%rsp,RootName=>'xcatresponse',NoAttr=>1,KeyAttr=>[]);
    $cb->(\%rsp);
    return;
}

# getTablesNodesAttribs
# Read the nodes in the noderange from the input tables
# and get the input attributes
# or get ALL attributes, if the word ALL is used.
# If the <attr>ALL</attr> is input then read all the attributes
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getTablesNodesAttribs</command>
#<noderange>blade01-blade10</noderange>
#<table>
#<tablename>nodelist</tablename>
#<attr>groups</attr>
#<attr>status</attr>
#</table>
#<table>
#<tablename>nodetype</tablename>
#<attr>ALL</attr>
#</table>
#   .
#   .
#   .
#</xcatrequest>
#
#<xcatresponse>
#<table>
#<tablename>tablename1</tablename>
#<node>
#<name>n1</name>
#<attr1>value1</attr1>
#<attr2>value1</attr2>
#.
#<attrN>valueN</attrN>
#</node>
#</table>
#   .
#   .
#   .
#</xcatresponse>
#
sub getTablesNodesAttribs
{
    my $request = shift;
    my $cb      = shift;
    my $command = $request->{command}->[0];
    my %rsp;

    # process each table in the request
    my $tables = $request->{table};
    my $node   = $request->{node};
    my @nodes  = @$node;
    foreach my $tabhash (@$tables) {

        my $tablename = $tabhash->{tablename}->[0];
        my $attr      = $tabhash->{attr};
        my @attrs     = @$attr;
        my $tab       = xCAT::Table->new($tablename);
        my %noderecs;
        my $recs;

        # build the table name record
        #@{$noderecs{table}->[0]->{tablename}} = $tablename;
        # if request for ALL attributes
        if (grep (/ALL/, @attrs)) { # read the  schema and build array of all attrs
            @attrs = ();
            my $schema = xCAT::Table->getTableSchema($tablename);
            my $desc   = $schema->{descriptions};
            foreach my $c (@{ $schema->{cols} }) {

                # my $space = (length($c)<7 ? "\t\t" : "\t");
                push @attrs, $c;
            }
        }

        # read the nodes and their attributes in this table
        my $rechash = $tab->getNodesAttribs(\@nodes, \@attrs);
        foreach my $node (@nodes) {
            my $recs = $rechash->{$node};
            foreach my $rec (@$recs) {
                my %datseg = ();
                $datseg{name} = [$node];
                foreach my $key (keys %$rec) {
                    if ($key ne "node") { # do not put in the added node attribute
                        $datseg{$key} = [ $rec->{$key} ];
                    }
                }
                push @{ $noderecs{table}->[0]->{node} }, \%datseg;
            }

        }
        @{ $noderecs{table}->[0]->{tablename} } = $tablename;
        push @{ $rsp{"table"} }, @{ $noderecs{table} };
    }    # end of all table processing

    # for checkin XML created
    #my  $xmlrec=XMLout(\%rsp,RootName=>'xcatresponse',NoAttr=>1,KeyAttr=>[]);
    $cb->(\%rsp);
    return;
}

# getTablesALLRowAttribs
# Read  all the rows from the input non-Node key'd
# tables and get the input attributes
# or get ALL attributes, if the word ALL is used.
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getTablesALLRowAttribs</command>
#<table>
#<tablename>osimage</tablename>
#<attr>imagename</attr>
#<attr>synclists</attr>
#</table>
#<table>
#<tablename>linuximage</tablename>
#<attr>ALL</attr>
#</table>
#   .
#   .
#   .
#</xcatrequest>
#
#<xcatresponse>
#<table>
#<tablename>osimage</tablename>
#<row>
#<synclists>value1</synclists>
#</row>
#<row>
#.
#.
#</row>
#</table>
#<table>
#<tablename>linuximage</tablename>
#<row>
#<imagename>value</imagename>
#<template>value</template>
#.
#.
#</row>
#<row>
#.
#.
#</row>
#</table>
#</xcatresponse>
#.
#.
#
sub getTablesAllRowAttribs
{
    my $request = shift;
    my $cb      = shift;
    my $command = $request->{command}->[0];
    my %rsp;

    # process each table in the request
    my $tables = $request->{table};
    foreach my $tabhash (@$tables) {

        my $tablename = $tabhash->{tablename}->[0];
        my $attr      = $tabhash->{attr};
        my @attrs     = @$attr;
        my $tab       = xCAT::Table->new($tablename);
        my %tblrecs;

        # build the table name record
        @{ $tblrecs{table}->[0]->{tablename} } = $tablename;

        # if request for ALL attributes
        if (grep (/ALL/, @attrs)) { # read the  schema and build array of all attrs
            @attrs = ();
            my $schema = xCAT::Table->getTableSchema($tablename);
            my $desc   = $schema->{descriptions};
            foreach my $c (@{ $schema->{cols} }) {

                # my $space = (length($c)<7 ? "\t\t" : "\t");
                push @attrs, $c;
            }
        }

        # read all the attributes in this table
        my @recs = $tab->getAllAttribs(@attrs);
        foreach my $rec (@recs) {
            my %datseg = ();
            foreach my $key (keys %$rec) {
                $datseg{$key} = $rec->{$key};
            }
            push @{ $tblrecs{table}->[0]->{row} }, \%datseg;
        }
        push @{ $rsp{"table"} }, @{ $tblrecs{table} };
    }    # end of all table processing
         # for checkin XML created
     # my  $xmlrec=XMLout(\%rsp,RootName=>'xcatresponse',NoAttr=>1,KeyAttr=>[]);
    $cb->(\%rsp);
    return;
}
#
# setNodesAttribs - setNodesAttribs
# Sets Nodes attributes for noderange for each of the tables supplied
# Example of XML in for this routine
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>setNodesAttribs</command>
#<noderange>blade01-blade02</noderange>
#<arg>
#   <table>
#      <name>nodelist</name>
#      <attr>
#         <groups>test</groups>
#         <comments> This is a another testx</comments>
#      </attr>
#   </table>
#   <table>
#      <name>nodetype</name>
#      <attr>
#         <os>Redhat2</os>
#         <comments> This is a another testy</comments>
#      </attr>
#   </table>
#</arg>
#</xcatrequest>
#
sub setNodesAttribs
{
    my $request   = shift;
    my $cb        = shift;
    my $node      = $request->{node};           # added by Client.pm
    my $noderange = $request->{noderange};
    my $command   = $request->{command}->[0];
    my %rsp;
    my $args   = $request->{arg};
    my $tables = $args->[0]->{table};

    # take input an build a request for the nodech function
    my $newrequest;
    $newrequest->{noderange} = $request->{noderange};
    $newrequest->{command}->[0] = "nodech";
    foreach my $table (@$tables) {
        my $tablename = $table->{name}->[0];
        my %keyhash;
        my $attrs = $table->{attr};
        foreach my $attrhash (@$attrs) {
            foreach my $key (keys %$attrhash) {
                my $tblattr = $tablename;
                $tblattr .= ".$key=";
                $tblattr .= $table->{attr}->[0]->{$key}->[0];
                push(@{ $newrequest->{arg} }, $tblattr);
            }
        }
    }

    # nodech will open the table and do all the work
    if (@$node) {
        &nodech(\@$node, $newrequest->{arg}, $cb, 0);
    } else {
        my $rsp = { errorcode => 1, error => "No nodes in noderange" };
        $cb->(\%rsp);
    }
    return;
}
#
# delEntries
# Deletes the table entry based on the input attributes
# The attributes and AND'd to together to form the delete request
# DELETE FROM nodelist WHERE ("groups" = "compute1,test" AND "status" = "down")
# Example of XML in for this routine
#
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>delEntries</command>
#<table>
#      <name>nodelist</name>
#      <attr>
#         <groups>compute1,test</groups>
#         <status>down</status>
#      </attr>
#</table>
#  .
#  .
#<table>
#  .
#  .
#  .
#</table>
#</xcatrequest>
#
# To delete all entries in a table, you input no attributes
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>delEntries</command>
#<table>
#      <name>nodelist</name>
#</table>
#</xcatrequest>


sub delEntries
{
    my $request = shift;
    my $cb      = shift;
    my $command = $request->{command}->[0];
    my %rsp;
    my $tables = $request->{table};
    foreach my $table (@$tables) {
        my $tablename = $table->{name}->[0];
        my $tab       = xCAT::Table->new($tablename);
        my %keyhash;
        my $attrs = $table->{attr};
        foreach my $attrhash (@$attrs) {
            foreach my $key (keys %$attrhash) {
                $keyhash{$key} = $attrhash->{$key}->[0];
            }
        }
        if (%keyhash) {    # delete based on requested attributes
            $tab->delEntries(\%keyhash);    #Yes, delete *all* entries
        } else {                            # delete all entries
            $tab->delEntries();             #delete *all* entries
        }
        $tab->commit;                       #  commit
    }
    return;
}

# getAttribs
# Read and returns array of  attributes for the key  from the input table.
#  and attributes input.  Use "ALL" in the <attr>ALL</attr> for all attributes
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>getAttribs</command>
#<table>site</table>
#<keys>
#  <key>domain</key>
#</keys>
#<attr>value</attr>
#<attr>comments</attr>
#</xcatrequest>
#
#
#<xcatresponse>
#<value>{domain value}</value>
#<comments>This is a comment</comments>
#</xcatresponse>
sub getAttribs
{
    my $request   = shift;
    my $cb        = shift;
    my $command   = $request->{command}->[0];
    my $tablename = $request->{table}->[0];
    my $attr      = $request->{attr};
    my @attrs     = @$attr;
    my $tab       = xCAT::Table->new($tablename);
    my %rsp;
    my %keyhash;

    # if request for ALL attributes
    if (grep (/ALL/, @attrs)) {  # read the  schema and build array of all attrs
        @attrs = ();
        my $schema = xCAT::Table->getTableSchema($tablename);
        my $desc   = $schema->{descriptions};
        foreach my $c (@{ $schema->{cols} }) {

            # my $space = (length($c)<7 ? "\t\t" : "\t");
            push @attrs, $c;
        }
    }
    foreach my $k (keys %{ $request->{keys}->[0] }) {
        $keyhash{$k} = $request->{keys}->[0]->{$k}->[0];
    }
    my $recs = $tab->getAttribs(\%keyhash, \@attrs);

    if ($recs) {
        my %attrhash = %$recs;
        foreach my $k (keys %attrhash) {

            push @{ $rsp{$k} }, $recs->{$k};
        }
    }
    $cb->(\%rsp);
    return;
}

# setAttribs
# Set the  attributes for the key(s) input in the table.
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>setAttribs</command>
#<table>site</table>
#<keys>
#  <key>domain</key>
#</keys>
#<attr>
#  <value>cluster.net</value>
#  <comments>This is a comment</comments>
#</xcatrequest>
#
#
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>setAttribs</command>
#<table>networks</table>
#<keys>
#  <net>10.0.1.0</net>
#  <mask>255.255.255.0</mask>
#</keys>
#<attr>
#  <netname>mynet</netname>
#  <gateway>10.0.1.254</gateway>
#</attr>
#</xcatrequest>

sub setAttribs
{
    my $request   = shift;
    my $cb        = shift;
    my $command   = $request->{command}->[0];
    my $tablename = $request->{table}->[0];
    my $tab       = xCAT::Table->new($tablename);
    my %rsp;
    my %keyhash;
    my %attrhash;

    foreach my $k (keys %{ $request->{keys}->[0] }) {
        $keyhash{$k} = $request->{keys}->[0]->{$k}->[0];
    }
    foreach my $a (keys %{ $request->{attr}->[0] }) {
        $attrhash{$a} = $request->{attr}->[0]->{$a}->[0];
    }
    $tab->setAttribs(\%keyhash, \%attrhash);
    return;
}

# noderange
# Expands the input noderange into a list of nodes.
#<xcatrequest>
#<clienttype>PCM</clienttype>
#<command>noderange</command>
#<noderange>compute1-compute2</noderange>
#</xcatrequest>
#<xcatresponse>
#<node>nodename1</node>
# .
# .
#<node>nodenamern1</node>
#</xcatresponse>
sub NodeRange
{
    my $request = shift;
    my $cb      = shift;
    my $command = $request->{command}->[0];
    my %rsp;
    my $node  = $request->{node};
    my @nodes = @$node;
    foreach my $node (@nodes) {
        push @{ $rsp{"node"} }, $node;

    }
    $cb->(\%rsp);
    return;
}