2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-22 15:48:14 +00:00

Merge pull request #7949 from VersatusHPC/fix/table-commands

fix(table): refuse extra tabrestore fields and unknown chtab -d tables
This commit is contained in:
Daniel Hilst
2026-10-16 18:17:31 -03:00
committed by GitHub
5 changed files with 145 additions and 8 deletions
+33
View File
@@ -2171,4 +2171,37 @@ sub rmnodegroups {
$tabhd->setNodeAttribs($node, { groups => join(",", @newgroups) });
}
#-----------------------------------------------------------------------------
=head3 delete_rows
delete the rows that match the keys from each named table
Arguments:
keys: a hash reference of key column values
names: the table names
Returns:
undef, or the name of the first table that does not open
Example:
my $missing = xCAT::TableUtils->delete_rows({ node => 'n1' }, 'nodehm', 'noderes');
=cut
#-----------------------------------------------------------------------------
sub delete_rows {
my ($class, $keys, @names) = @_;
# Every table opens before the first delete, so a table that does not open leaves all of them unchanged.
my %tables;
foreach my $name (@names) {
$tables{$name} = xCAT::Table->new($name, -create => 1, -autocommit => 0) or return $name;
}
foreach my $name (@names) {
$tables{$name}->delEntries($keys);
$tables{$name}->commit;
}
return;
}
1;
+5 -4
View File
@@ -430,6 +430,7 @@ sub tabrestore
my $origline = $line; #save for error reporting
my %record;
my $col;
my $separated; #a comma after the field of the last column starts one more field
foreach $col (@colns)
{
if ($line =~ /^,/ or $line eq "")
@@ -438,7 +439,7 @@ sub tabrestore
if (!exists($auto_cols{$col})) {
$record{$col} = undef;
}
$line =~ s/^,//;
$separated = ($line =~ s/^,//);
}
elsif ($line =~ /^[^,]*"/)
{ # We have stuff in quotes... pain...
@@ -501,7 +502,7 @@ sub tabrestore
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/^,//;
$separated = ($line =~ s/^,//);
chop $ent;
$ent = substr($ent, 1);
$ent =~ s/""/"/g;
@@ -531,10 +532,10 @@ sub tabrestore
{
$record{$col} = $1;
}
$line =~ s/^([^,]+)(,|$)//;
$separated = ($line =~ s/^([^,]+)(,|$)//) && $2 eq ',';
}
}
if ($line)
if ($line ne '' or $separated)
{
$rollback = 1;
$cb->({ error => "Too many fields on line $linenumber: $origline | $line", errorcode => 4 });
+5 -4
View File
@@ -6,6 +6,7 @@ BEGIN
}
use lib "$::XCATROOT/lib/perl";
use xCAT::Table;
use xCAT::TableUtils;
use xCAT::NodeRange;
use Getopt::Long;
@@ -97,10 +98,10 @@ if ($::DELETE) {
usage;
exit(1);
}
for (@tables_to_del) {
$tables{$_} = xCAT::Table->new($_, -create => 1, -autocommit => 0);
$tables{$_}->delEntries(\%keyhash);
$tables{$_}->commit;
my $missing = xCAT::TableUtils->delete_rows(\%keyhash, @tables_to_del);
if (defined($missing)) {
print "Table $missing does not exist.\n";
exit(1);
}
}
else {
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use Test::More;
my $fixture = tempdir(CLEANUP => 1);
make_path("$fixture/db");
local $ENV{XCATCFG} = "SQLite:$fixture/db";
require xCAT::Table;
require xCAT::TableUtils;
for my $table (qw(nodehm noderes)) {
xCAT::Table->new($table, -create => 1)->setAttribs({ node => $_ }, { comments => "row of $_" }) for qw(n1 n2 n3);
}
sub nodes {
my ($table) = @_;
return join(',', sort map { $_->{node} } xCAT::Table->new($table)->getAllAttribs('node'));
}
sub delete_rows {
my @args = @_;
my $missing = eval { xCAT::TableUtils->delete_rows(@args) };
return ($missing, $@);
}
my ($missing, $error) = delete_rows({ node => 'n1' }, 'nodehm', 'noderes');
is($error, '', 'rows are deleted from tables that open');
is($missing, undef, 'no table is reported when every table opens');
is(nodes('nodehm'), 'n2,n3', 'the row of the key is deleted from the first table');
is(nodes('noderes'), 'n2,n3', 'the row of the key is deleted from the last table');
for my $case ([ 'between two tables', 'nodehm', 'notatable', 'noderes' ], [ 'first', 'notatable', 'nodehm', 'noderes' ],
[ 'last', 'nodehm', 'noderes', 'notatable' ]) {
my ($label, @tables) = @$case;
($missing, $error) = delete_rows({ node => 'n2' }, @tables);
is($error, '', "a missing table named $label does not die");
is($missing, 'notatable', "a missing table named $label is returned");
is(nodes('nodehm'), 'n2,n3', "nodehm keeps its rows when a missing table is named $label");
is(nodes('noderes'), 'n2,n3', "noderes keeps its rows when a missing table is named $label");
}
done_testing();
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl";
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use Test::More;
use XCAT::Test::File qw(repo_path);
my $fixture = tempdir(CLEANUP => 1);
make_path("$fixture/db");
local $ENV{XCATCFG} = "SQLite:$fixture/db";
require xCAT::Table;
require(repo_path('xCAT-server/lib/xcat/plugins/tabutils.pm'));
# Restores the rows into the site table, which has 4 columns, over a row that was there before.
sub restore {
my @rows = @_;
my $site = xCAT::Table->new('site', -create => 1);
$site->delEntries();
$site->setAttribs({ key => 'before' }, { value => 'kept' });
my @errors;
xCAT_plugin::tabutils::process_request({ command => ['tabrestore'], table => ['site'], data => [ '#key,value,comments,disable', @rows ] },
sub { push @errors, map { ref($_) ? @$_ : $_ } grep { defined } $_[0]{error} });
my %rows = map { ($_->{key} => join('|', map { $_ // '' } @{$_}{qw(value comments)})) } xCAT::Table->new('site')->getAllAttribs('key', 'value', 'comments');
return (join("\n", @errors), \%rows);
}
for my $case (
[ 'one field for each column', '"k1","v1",,' ],
[ 'fewer fields than columns', '"k1","v1"' ],
[ 'a quoted field that holds a comma', '"k1","v1","a,b",', 'v1|a,b' ],
[ 'a quoted comment before the last field', '"k1","v1","note",', 'v1|note' ])
{
my ($label, $row, $want) = @$case;
my ($errors, $rows) = restore($row);
is($errors, '', "$label: the row is restored without an error");
is_deeply($rows, { k1 => $want // 'v1|' }, "$label: the restore replaces the table with the row");
}
for my $case (
[ 'an empty field after the last column', '"k1","v1",,,' ],
[ 'a field of 0 after the last column', '"k1","v1",,,0' ],
[ 'an empty field after a quoted last column', '"k1","v1",,"yes",' ],
[ 'an empty field after an unquoted last column', '"k1","v1",,yes,' ],
[ 'a text field after the last column', '"k1","v1",,,extra' ])
{
my ($label, $row) = @$case;
my ($errors, $rows) = restore('"k0","v0",,', $row);
like($errors, qr/Too many fields on line \d+: \Q$row\E/, "$label: the row is refused");
is_deeply($rows, { before => 'kept|' }, "$label: the restore is rolled back");
}
done_testing();