2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-08-28 01:26:41 +00:00
Files
xcat-core/xCAT-test/unit/genesis_openembedded_hardware.t
T
2026-08-20 22:29:56 -03:00

382 lines
14 KiB
Perl

#!/usr/bin/env perl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use JSON::PP;
use Test::More;
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
my $dispatcher = File::Spec->catfile(
$repo_root,
qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-hardware-control files genesis-hardware)
);
my $nvme_provider = File::Spec->catfile(
$repo_root,
qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-hardware-control files provider-nvme)
);
my $mstflint_provider = File::Spec->catfile(
$repo_root,
qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-hardware-control files provider-mstflint)
);
my $iprutils_provider = File::Spec->catfile(
$repo_root,
qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-hardware-control files provider-iprutils)
);
sub write_file {
my ( $path, $contents, $mode ) = @_;
open( my $fh, '>', $path ) or die "Unable to write $path: $!";
print {$fh} $contents;
close($fh);
chmod( $mode, $path ) if defined($mode);
}
sub read_file {
my ($path) = @_;
open( my $fh, '<', $path ) or die "Unable to read $path: $!";
my $contents = do { local $/; <$fh> };
close($fh);
return $contents;
}
sub shell_quote {
my ($value) = @_;
$value =~ s/'/'"'"'/g;
return "'$value'";
}
sub run_command {
my ( $environment, @command ) = @_;
local %ENV = ( %ENV, %{$environment} );
my $shell_command = join( ' ', map { shell_quote($_) } @command );
my $output = qx{$shell_command 2>&1};
return ( $? >> 8, $output );
}
sub decode_output {
my ($output) = @_;
return JSON::PP->new->decode($output);
}
my $root = tempdir( CLEANUP => 1 );
my $bin = File::Spec->catdir( $root, 'bin' );
my $manifests = File::Spec->catdir( $root, 'manifests' );
my $executables = File::Spec->catdir( $root, 'providers' );
my $audit = File::Spec->catfile( $root, 'audit', 'hardware.jsonl' );
my $request = File::Spec->catfile( $root, 'request.json' );
make_path( $bin, $manifests, $executables );
write_file(
File::Spec->catfile( $bin, 'timeout' ),
<<'SH', 0755
#!/bin/sh
while [ "$#" -gt 0 ]; do
case "$1" in
--signal=*|--kill-after=*) shift ;;
*) break ;;
esac
done
shift
exec "$@"
SH
);
write_file( File::Spec->catfile( $bin, 'flock' ), "#!/bin/sh\nexit 0\n", 0755 );
write_file(
File::Spec->catfile( $executables, 'mock' ),
<<'SH', 0755
#!/bin/sh
case "$1" in
probe)
printf '%s\n' '{"detected":true,"devices":["controller0"]}'
;;
run)
case "$2" in
storage.inventory) printf '%s\n' '{"devices":["controller0"]}' ;;
storage.array.create) printf '{"created":"%s"}\n' "$3" ;;
storage.array.delete) printf '%s\n' 'delete failed' >&2; exit 9 ;;
storage.logs) printf '%s\n' 'not-json' ;;
*) exit 8 ;;
esac
;;
*) exit 7 ;;
esac
SH
);
write_file(
File::Spec->catfile( $manifests, 'mock.json' ),
JSON::PP->new->canonical->pretty->encode(
{
capabilities => [
{ destructive => JSON::PP::false, name => 'storage.inventory' },
{ destructive => JSON::PP::false, name => 'storage.logs' },
{ destructive => JSON::PP::true, name => 'storage.array.create' },
{ destructive => JSON::PP::true, name => 'storage.array.delete' },
],
kind => 'storage',
name => 'mock',
schema => 1,
version => '1.0',
}
)
);
write_file( $request, "{\"level\":\"1\"}\n" );
my %environment = (
PATH => "$bin:$ENV{PATH}",
XCAT_GENESIS_HARDWARE_AUDIT => $audit,
XCAT_GENESIS_PROVIDER_DIR => $manifests,
XCAT_GENESIS_PROVIDER_EXEC_DIR => $executables,
XCAT_GENESIS_PROVIDER_TIMEOUT => 5,
);
my ( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'providers' );
is( $status, 0, 'provider list is accepted' ) or diag($output);
is( decode_output($output)->{providers}->[0]->{name}, 'mock',
'provider list returns the manifest' );
( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'capabilities', 'mock' );
is( $status, 0, 'provider capabilities are accepted' ) or diag($output);
is( scalar @{ decode_output($output)->{capabilities} }, 4,
'provider capabilities are returned' );
( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'probe', 'mock' );
is( $status, 0, 'provider probe succeeds' ) or diag($output);
ok( decode_output($output)->{probes}->[0]->{result}->{detected},
'provider probe returns detected hardware' );
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock', 'storage.inventory'
);
is( $status, 0, 'read-only capability succeeds' ) or diag($output);
is( decode_output($output)->{result}->{devices}->[0], 'controller0',
'read-only result is normalized' );
ok( !-e $audit, 'read-only capability is not audited as a change' );
for my $case (
[ [], 'destructive capability requires a task identity' ],
[ [ '--task-id', 'task-1', '--request', $request ],
'destructive capability requires an exact device' ],
[ [ '--task-id', 'task-1', '--device-id', 'controller0' ],
'destructive capability requires a request' ],
[ [ '--task-id', 'task-1', '--device-id', 'all', '--request', $request ],
'destructive capability rejects a wildcard device' ],
)
{
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock',
'storage.array.create', @{ $case->[0] }
);
isnt( $status, 0, $case->[1] );
}
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock',
'storage.array.create', '--task-id', 'task-1', '--device-id', 'controller0',
'--request', $request
);
is( $status, 0, 'authorized destructive capability succeeds' ) or diag($output);
is( decode_output($output)->{result}->{created}, 'controller0',
'destructive result is normalized' );
my @audit_records = map { decode_output($_) } split( /\n/, read_file($audit) );
is_deeply( [ map { $_->{phase} } @audit_records ], [ 'started', 'completed' ],
'successful change has a closed audit trail' );
is( $audit_records[0]->{task_id}, 'task-1',
'audit record carries the task identity' );
like( $audit_records[0]->{request_sha256}, qr/^[0-9a-f]{64}$/,
'audit record carries the request digest' );
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock',
'storage.array.delete', '--task-id', 'task-2', '--device-id', 'controller0',
'--request', $request
);
isnt( $status, 0, 'provider failure is returned' );
@audit_records = map { decode_output($_) } split( /\n/, read_file($audit) );
is_deeply( [ map { $_->{phase} } @audit_records ],
[ 'started', 'completed', 'started', 'failed' ],
'failed change has a closed audit trail' );
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock', 'storage.logs'
);
isnt( $status, 0, 'invalid provider JSON is rejected' );
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock', 'storage.unknown'
);
isnt( $status, 0, 'undeclared capability is rejected' );
my $linked_manifest = File::Spec->catfile( $manifests, 'linked.json' );
symlink( File::Spec->catfile( $manifests, 'mock.json' ), $linked_manifest )
or die "Unable to create $linked_manifest: $!";
( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'providers' );
isnt( $status, 0, 'linked provider manifest is rejected' );
unlink($linked_manifest) or die "Unable to remove $linked_manifest: $!";
my $invalid_manifest = File::Spec->catfile( $manifests, 'invalid.json' );
write_file( $invalid_manifest, "{}\n" );
( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'providers' );
isnt( $status, 0, 'invalid provider manifest is rejected' );
unlink($invalid_manifest) or die "Unable to remove $invalid_manifest: $!";
my $mock_executable = File::Spec->catfile( $executables, 'mock' );
my $real_executable = File::Spec->catfile( $executables, 'mock.real' );
rename( $mock_executable, $real_executable )
or die "Unable to rename $mock_executable: $!";
symlink( $real_executable, $mock_executable )
or die "Unable to link $mock_executable: $!";
( $status, $output ) =
run_command( \%environment, '/bin/bash', $dispatcher, 'probe', 'mock' );
isnt( $status, 0, 'linked provider executable is rejected' );
unlink($mock_executable) or die "Unable to remove $mock_executable: $!";
rename( $real_executable, $mock_executable )
or die "Unable to restore $mock_executable: $!";
my $linked_request = File::Spec->catfile( $root, 'linked-request.json' );
symlink( $request, $linked_request ) or die "Unable to link $linked_request: $!";
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock',
'storage.array.create', '--task-id', 'task-3', '--device-id', 'controller0',
'--request', $linked_request
);
isnt( $status, 0, 'linked hardware request is rejected' );
my %bad_timeout = ( %environment, XCAT_GENESIS_PROVIDER_TIMEOUT => 0 );
( $status, $output ) =
run_command( \%bad_timeout, '/bin/bash', $dispatcher, 'providers' );
isnt( $status, 0, 'invalid provider timeout is rejected' );
unlink("$audit.lock") or die "Unable to remove $audit.lock: $!";
my $lock_target = File::Spec->catfile( $root, 'lock-target' );
write_file( $lock_target, "unchanged\n" );
symlink( $lock_target, "$audit.lock" ) or die "Unable to link audit lock: $!";
( $status, $output ) = run_command(
\%environment, '/bin/bash', $dispatcher, 'run', 'mock',
'storage.array.create', '--task-id', 'task-3', '--device-id', 'controller0',
'--request', $request
);
isnt( $status, 0, 'linked audit lock is rejected' );
is( read_file($lock_target), "unchanged\n", 'linked audit target is untouched' );
write_file(
File::Spec->catfile( $bin, 'nvme' ),
"#!/bin/sh\nprintf '{\"arguments\":\"%s\"}\\n' \"\$*\"\n", 0755
);
my $sys_nvme = File::Spec->catdir( $root, 'sys', 'class', 'nvme' );
my $dev = File::Spec->catdir( $root, 'dev' );
make_path( File::Spec->catdir( $sys_nvme, 'nvme0' ), $dev );
write_file( File::Spec->catfile( $dev, 'nvme0' ), '' );
my %nvme_environment = (
PATH => "$bin:$ENV{PATH}",
XCAT_GENESIS_DEV_DIR => $dev,
XCAT_GENESIS_SYS_CLASS_NVME => $sys_nvme,
);
( $status, $output ) =
run_command( \%nvme_environment, '/bin/bash', $nvme_provider, 'probe' );
is( $status, 0, 'NVMe probe succeeds' ) or diag($output);
is( decode_output($output)->{devices}->[0], 'nvme0',
'NVMe probe returns an exact controller identity' );
( $status, $output ) = run_command(
\%nvme_environment, '/bin/bash', $nvme_provider, 'run', 'storage.health',
'nvme0', '-'
);
is( $status, 0, 'NVMe health query succeeds' ) or diag($output);
like( decode_output($output)->{arguments}, qr{smart-log .*/nvme0 -o json},
'NVMe health query uses the selected controller' );
( $status, $output ) = run_command(
\%nvme_environment, '/bin/bash', $nvme_provider, 'run', 'storage.health',
'nvme0n1', '-'
);
isnt( $status, 0, 'NVMe provider rejects a namespace identity' );
write_file(
File::Spec->catfile( $bin, 'mstflint' ),
"#!/bin/sh\nprintf '%s\\n' 'firmware query'\n", 0755
);
my $pci = File::Spec->catdir( $root, 'sys', 'bus', 'pci', 'devices' );
my $mellanox = File::Spec->catdir( $pci, '0000:03:00.0' );
my $other = File::Spec->catdir( $pci, '0000:04:00.0' );
make_path( $mellanox, $other );
write_file( File::Spec->catfile( $mellanox, 'vendor' ), "0x15b3\n" );
write_file( File::Spec->catfile( $other, 'vendor' ), "0x8086\n" );
my %mstflint_environment = (
PATH => "$bin:$ENV{PATH}",
XCAT_GENESIS_SYS_PCI_DEVICES => $pci,
);
( $status, $output ) =
run_command( \%mstflint_environment, '/bin/bash', $mstflint_provider, 'probe' );
is( $status, 0, 'mstflint probe succeeds' ) or diag($output);
is_deeply( decode_output($output)->{devices}, ['0000:03:00.0'],
'mstflint probe returns supported PCI devices' );
( $status, $output ) = run_command(
\%mstflint_environment, '/bin/bash', $mstflint_provider, 'run',
'network.firmware.inventory', '0000:03:00.0', '-'
);
is( $status, 0, 'mstflint inventory succeeds' ) or diag($output);
is( decode_output($output)->{raw}, 'firmware query',
'mstflint inventory returns the tool output' );
( $status, $output ) = run_command(
\%mstflint_environment, '/bin/bash', $mstflint_provider, 'run',
'network.firmware.inventory', '0000:04:00.0', '-'
);
isnt( $status, 0, 'mstflint provider rejects another vendor' );
write_file(
File::Spec->catfile( $bin, 'iprconfig' ),
<<'SH', 0755
#!/bin/sh
case "$*" in
'-c show-config') printf '%s\n' 'Power RAID configuration' ;;
'-c show-arrays') printf '%s\n' 'Power RAID arrays healthy' ;;
'-c show-ucode-levels') printf '%s\n' 'Power RAID firmware levels' ;;
*) exit 2 ;;
esac
SH
);
my $scsi_hosts = File::Spec->catdir( $root, 'sys', 'class', 'scsi_host' );
my $ipr_host = File::Spec->catdir( $scsi_hosts, 'host0' );
my $other_host = File::Spec->catdir( $scsi_hosts, 'host1' );
make_path( $ipr_host, $other_host );
write_file( File::Spec->catfile( $ipr_host, 'proc_name' ), "ipr\n" );
write_file( File::Spec->catfile( $other_host, 'proc_name' ), "megaraid_sas\n" );
my %iprutils_environment = (
PATH => "$bin:$ENV{PATH}",
XCAT_GENESIS_SYS_SCSI_HOSTS => $scsi_hosts,
);
( $status, $output ) =
run_command( \%iprutils_environment, '/bin/bash', $iprutils_provider, 'probe' );
is( $status, 0, 'iprutils probe succeeds' ) or diag($output);
is_deeply( decode_output($output)->{devices}, ['host0'],
'iprutils probe returns Power RAID hosts' );
for my $case (
[ 'storage.inventory', 'Power RAID configuration' ],
[ 'storage.health', 'Power RAID arrays healthy' ],
[ 'storage.firmware.inventory', 'Power RAID firmware levels' ],
)
{
( $status, $output ) = run_command(
\%iprutils_environment, '/bin/bash', $iprutils_provider, 'run',
$case->[0], 'host0', '-'
);
is( $status, 0, "iprutils $case->[0] succeeds" ) or diag($output);
is( decode_output($output)->{raw}, $case->[1],
"iprutils $case->[0] returns the tool output" );
}
( $status, $output ) = run_command(
\%iprutils_environment, '/bin/bash', $iprutils_provider, 'run',
'storage.array.create', 'host0', '-'
);
isnt( $status, 0, 'iprutils provider rejects destructive operations' );
done_testing();