mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-08-28 01:26:41 +00:00
382 lines
14 KiB
Perl
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();
|