# 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 xCAT::Table; use xCAT::Schema; use Data::Dumper; use xCAT::NodeRange; use xCAT::Schema; use Getopt::Long; 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", tabrestore => "tabutils", tabch => "tabutils", # not implemented yet 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 tabgrep => "tabutils" }; } # Each cmd now returns its own usage inside its function #my %usage = ( #nodech => "Usage: nodech [table.column=value] [table.column=value] ...", #nodeadd => "Usage: nodeadd [table.column=value] [table.column=value] ...", #noderm => "Usage: noderm ", # the usage for tabdump is in the tabdump function #tabdump => "Usage: tabdump \n where is one of the following:\n " . join("\n ", keys %xCAT::Schema::tabspec), # the usage for tabrestore is in the tabrestore client cmd #tabrestore => "Usage: tabrestore .csv", #); ##################################################### # 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; 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 "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 "nodech" or $command eq "nodech") { return nodech($nodes, $args, $callback, 0); } elsif ($command eq "tabrestore") { return tabrestore($request, $callback); } elsif ($command eq "tabdump") { return tabdump($args, $callback); } elsif ($command eq "gettab") { return gettab($request, $callback); } elsif ($command eq "tabgrep") { return tabgrep($nodes, $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; sub gettab_usage { my $exitcode = shift @_; my %rsp; push @{$rsp{data}}, "Usage: gettab key=value,... table.attribute ..."; push @{$rsp{data}}, " gettab [-?|-h|--help]"; if ($exitcode) { $rsp{errorcode} = $exitcode; } $callback->(\%rsp); } # Process arguments @ARGV = @{$req->{arg}}; if (!GetOptions('h|?|help' => \$HELP)) { 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; 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}}); foreach my $coln (keys %{$tabhash{$tabn}}) { if ($terse) { $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; sub noderm_usage { 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); } @ARGV = @{$args}; if (!GetOptions('h|?|help' => \$HELP, 'v|version' => \$VERSION) ) { noderm_usage(1); return; } if ($HELP) { noderm_usage(0); return; } if ($VERSION) { my %rsp; $rsp->{data}->[0] = "2.0"; $cb->($rsp); return; } if (!$nodes) { noderm_usage(1); return; } # 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, $_; } } nodech($nodes, \@tablist, $cb, 0); } 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 $linenumber = 1; my $tab = xCAT::Table->new($table, -create => 1, -autocommit => 0); unless ($tab) { $cb->({error => "Unable to open $table",errorcode=>4}); return; } $tab->delEntries(); #Yes, delete *all* entries my $header = shift @{$request->{data}}; $header =~ s/"//g; #Strip " from overzealous CSV apps $header =~ s/^#//; $header =~ s/\s+$//; my @colns = split(/,/, $header); my $line; my $rollback = 0; LINE: foreach $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? $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) { #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; } $nextchar = substr($line, $offset, 1); if ($nextchar eq '"') { $offset++; } elsif ($offset eq length($line) or $nextchar eq ',') { $ent = substr($line, 0, $offset, ''); $line =~ s/^,//; chop $ent; $ent = substr($ent, 1); $ent =~ s/""/"/g; $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.. $record{$col} = $1; $line =~ s/^([^,]+)(,|$)//; } } if ($line) { $rollback = 1; $cb->({error => "Too many fields on line $linenumber: $origline | $line", errorcode=>4}); next LINE; } #TODO: 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 $table = ""; my $HELP; my $DESC; sub tabdump_usage { my $exitcode = shift @_; my %rsp; push @{$rsp{data}}, "Usage: tabdump [-d] [table]"; push @{$rsp{data}}, " tabdump [-?|-h|--help]"; if ($exitcode) { $rsp{errorcode} = $exitcode; } $cb->(\%rsp); } # Process arguments @ARGV = @{$args}; if (!GetOptions('h|?|help' => \$HELP, 'd' => \$DESC)) { tabdump_usage(1); return; } if ($HELP) { tabdump_usage(0); return; } 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 ($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; } $table = $ARGV[0]; 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); sub tabdump_header { 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; } my $recs = $tabh->getAllEntries(); 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 in the order of the columns in the schema tabdump_header(@{$tabh->{colnames}}); foreach $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 $line = $line . $lineappend; push @{$rsp{data}}, $line; } $cb->(\%rsp); } sub nodech { my $nodes = shift; my $args = shift; my $callback = shift; my $addmode = shift; my $VERSION; my $HELP; my $deletemode; sub nodech_usage { my $exitcode = shift @_; my $addmode = shift @_; my $cmdname = $addmode ? 'nodeadd' : 'nodech'; my %rsp; if ($addmode) { push @{$rsp{data}}, "Usage: $cmdname groups= [table.column=value] [...]"; } else { push @{$rsp{data}}, "Usage: $cmdname table.column=value [...]"; push @{$rsp{data}}, " $cmdname {-d | --delete} [...]"; } push @{$rsp{data}}, " $cmdname {-v | --version}"; push @{$rsp{data}}, " $cmdname [-? | -h | --help]"; if ($exitcode) { $rsp{errorcode} = $exitcode; } $callback->(\%rsp); } @ARGV = @{$args}; my %options = ('h|?|help' => \$HELP, 'v|version' => \$VERSION); if (!$addmode) { $options{'d|delete'} = \$deletemode; } if (!GetOptions(%options)) { nodech_usage(1, $addmode); return; } # Help if ($HELP) { nodech_usage(0, $addmode); return; } # Version if ($VERSION) { my %rsp; $rsp->{data}->[0] = "2.0"; $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; } if ($addmode) { my $nr = shift @ARGV; $nodes = [noderange($nr, 0)]; unless ($nodes) { $callback->({error => "No noderange to add.\n",errorcode=>1}); return; } } my $column; my $value; my $temp; my %tables; 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/=/) { $callback->({error => ["Malformed argument $_ ignored."],errorcode=>1}); next; } ($temp, $value) = split('=', $_, 2); my $op = '='; if ($temp =~ /,$/) { $op = ',='; chop($temp); } elsif ($temp =~ /\^$/) { $op = '^='; chop($temp); } 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); } foreach $tab (keys %tables) { my $tabhdl = xCAT::Table->new($tab, -create => 1, -autocommit => 0); if ($tabhdl) { foreach (@$nodes) { if ($deletemode) { $tabhdl->delEntries({'node' => $_}); } else { #$tabhdl->setNodeAttribs($_,$tables{$tab}); my %uhsh; my $node = $_; 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 = $_; if ($op eq '=') { $uhsh{$key} = $val; } elsif ($op eq ',=') { #splice assignment my $curval = $uhsh{$key}; # in case it was already set if (!defined($curval)) { my $cent = $tabhdl->getNodeAttribs($node, [$key]); if ($cent) { $curval = $cent->{$key}; } } if ($curval) { my @vals = split(/,/, $curval); unless (grep /^$val$/, @vals) { @vals = (@vals, $val); my $newval = join(',', @vals); $uhsh{$key} = $newval; } } else { $uhsh{$key} = $val; } } elsif ($op eq '^=') { my $curval = $uhsh{$key}; # in case it was already set if (!defined($curval)) { my $cent = $tabhdl->getNodeAttribs($node, [$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) { my @rc = $tabhdl->setNodeAttribs($node, \%uhsh); if (not defined($rc[0])) { $callback->({error => "DB error " . $rc[1],errorcode=>1}); } } } } $tabhdl->commit; } else { $callback->( {error => ["ERROR: Unable to open table $tab in configuration"],errorcode=>1} ); } } } sub tabgrep { my $node = shift; my @tablist; my $callback = shift; 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; } } ##################################################### # nodels command ##################################################### sub nodels { my $nodes = shift; my $args = shift; my $callback = shift; my $noderange = shift; my $VERSION; my $HELP; sub nodels_usage { my $exitcode = shift @_; my %rsp; push @{$rsp{data}}, "Usage:"; push @{$rsp{data}}, " nodels [noderange] [table.attribute | shortname] [...]"; push @{$rsp{data}}, " nodels {-v|--version}"; push @{$rsp{data}}, " nodels [-?|-h|--help]"; ##### xcat 1.2 nodels usage: # $rsp->{data}->[1]= " nodels [noderange] [group|pos|type|rg|install|hm|all]"; # $rsp->{data}->[2]= " "; # $rsp->{data}->[3]= " nodels [noderange] hm.{power|reset|cad|vitals|inv|cons}"; # $rsp->{data}->[4]= " hm.{bioscons|eventlogs|getmacs|netboot}"; # $rsp->{data}->[5]= " hm.{eth0|gcons|serialbios|beacon}"; # $rsp->{data}->[6]= " hm.{bootseq|serialbps|all}"; # $rsp->{data}->[7]= " "; # $rsp->{data}->[8]= " nodels [noderange] rg.{tftp|nfs_install|install_dir|serial}"; # $rsp->{data}->[9]= " rg.{usenis|install_roll|acct|gm|pbs}"; # $rsp->{data}->[10]=" rg.{access|gpfs|netdevice|prinic|all}"; if ($exitcode) { $rsp{errorcode} = $exitcode; } $callback->(\%rsp); } @ARGV = @{$args}; if (!GetOptions('h|?|help' => \$HELP, 'v|version' => \$VERSION,) ) { nodels_usage(1); return; } # Help if ($HELP) { nodels_usage(0); return; } # Version if ($VERSION) { my %rsp; $rsp->{data}->[0] = "2.0"; $callback->($rsp); return; } # TODO -- Parse command arguments # my $opt; # my %attrs; # foreach $opt (@ARGV) { # if ($opt =~ /^group/) { # } # } my $argc = @ARGV; my $terse = 2; 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 if ($argc) { my %tables; foreach (@ARGV) { my $table; my $column; my $temp = $_; 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]; #Mark this as something to get } } my $tab; my %noderecs; foreach $tab (keys %tables) { my $tabh = xCAT::Table->new($tab); unless ($tabh) { next; } #print Dumper($tables{$tab}); my $node; foreach $node (@$nodes) { my @cols; my %labels; foreach (@{$tables{$tab}}) { push @cols, $_->[0]; $labels{$_->[0]} = $_->[1]; } my $rec = $tabh->getNodeAttribs($node, \@cols); foreach (keys %$rec) { my %datseg; unless ($terse) { $datseg{data}->[0]->{desc} = [$labels{$_}]; } $datseg{data}->[0]->{contents} = [$rec->{$_}]; $datseg{name} = [$node]; #{}->{contents} = [$rec->{$_}]; push @{$noderecs{$node}}, \%datseg; } } #$rsp->{node}->[0]->{data}->[0]->{desc}->[0] = $_; #$rsp->{node}->[0]->{data}->[0]->{contents}->[0] = $_; $tabh->close(); undef $tabh; } foreach (sort (keys %noderecs)) { push @{$rsp->{"node"}}, @{$noderecs{$_}}; } } else { foreach (@$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); foreach (@ents) { my %rsp; if ($_->{node}) { $rsp->{node}->[0]->{name}->[0] = ($_->{node}); # $rsp->{node}->[0]->{data}->[0]->{contents}->[0]="$_->{node} node contents"; # $rsp->{node}->[0]->{data}->[0]->{desc}->[0]="$_->{node} node desc"; $callback->($rsp); } } } } return 0; }