diff --git a/xCAT-test/unit/genesis_openembedded_extensions.t b/xCAT-test/unit/genesis_openembedded_extensions.t new file mode 100644 index 000000000..f04f33cc6 --- /dev/null +++ b/xCAT-test/unit/genesis_openembedded_extensions.t @@ -0,0 +1,234 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Copy qw(copy); +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 $loader = File::Spec->catfile( + $repo_root, + qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-extensions files genesis-sysext) +); +my $signer = File::Spec->catfile( + $repo_root, qw(xCAT-genesis-builder oe scripts sign-extension) +); + +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 shell_quote { + my ($value) = @_; + $value =~ s/'/'"'"'/g; + return "'$value'"; +} + +sub run_command { + my (@command) = @_; + my $shell_command = join( ' ', map { shell_quote($_) } @command ); + my $output = qx{$shell_command 2>&1}; + return ( $? >> 8, $output ); +} + +sub write_manifest { + my ( $path, $hash, $changes ) = @_; + my $manifest = { + architecture => 'x86_64', + capabilities => ['diagnostic.smoke'], + genesis_release => '0.1', + kernel_modules => JSON::PP::false, + kernel_release => undef, + key_id => 'xcat-release', + license_class => 'open', + name => 'xcat-smoke', + pci_ids => [], + schema => 1, + sha256 => $hash, + version => '1.0', + }; + @{$manifest}{ keys %{$changes} } = values %{$changes}; + write_file( $path, JSON::PP->new->canonical->pretty->encode($manifest) ); +} + +my $root = tempdir( CLEANUP => 1 ); +my $bin = File::Spec->catdir( $root, 'bin' ); +my $keys = File::Spec->catdir( $root, 'keys' ); +my $run_dir = File::Spec->catdir( $root, 'run' ); +my $image = File::Spec->catfile( $root, 'xcat-smoke.raw' ); +my $manifest = File::Spec->catfile( $root, 'xcat-smoke.manifest.json' ); +my $signature = File::Spec->catfile( $root, 'xcat-smoke.sig' ); +my $private_key = File::Spec->catfile( $root, 'private.pem' ); +my $public_key = File::Spec->catfile( $keys, 'xcat-release.pem' ); +my $os_release = File::Spec->catfile( $root, 'os-release' ); +my $command_log = File::Spec->catfile( $root, 'commands.log' ); +my $status_log = File::Spec->catfile( $root, 'status.log' ); + +make_path( $bin, $keys, $run_dir ); +write_file( $image, "extension payload\n" ); +write_file( $os_release, "ID=xcat-genesis\nVERSION_ID=0.1\n" ); +write_file( $command_log, '' ); +write_file( $status_log, '' ); +write_file( + File::Spec->catfile( $bin, 'systemd-sysext' ), + "#!/bin/sh\nprintf 'systemd-sysext %s\\n' \"\$*\" >>\"\$XCAT_TEST_LOG\"\n", + 0755 +); +write_file( + File::Spec->catfile( $bin, 'genesis-status' ), + "#!/bin/sh\nprintf '%s\\n' \"\$*\" >>\"\$XCAT_STATUS_LOG\"\n", + 0755 +); + +is( system( 'openssl', 'genpkey', '-algorithm', 'ED25519', '-out', $private_key ) >> 8, + 0, 'test private key is generated' ); +is( system( 'openssl', 'pkey', '-in', $private_key, '-pubout', '-out', $public_key ) >> 8, + 0, 'test public key is generated' ); + +my ( $hash_status, $hash_output ) = run_command( 'sha256sum', '--', $image ); +is( $hash_status, 0, 'extension digest is generated' ); +my ($hash) = $hash_output =~ /^([0-9a-f]{64})/; +write_manifest( $manifest, $hash, {} ); + +my ( $sign_status, $sign_output ) = + run_command( $signer, $manifest, $private_key, $signature ); +is( $sign_status, 0, 'extension manifest is signed' ) or diag($sign_output); + +local %ENV = ( + %ENV, + PATH => "$bin:$ENV{PATH}", + XCAT_GENESIS_EXTENSION_KEY_DIR => $keys, + XCAT_GENESIS_EXTENSION_RUN_DIR => $run_dir, + XCAT_GENESIS_OS_RELEASE => $os_release, + XCAT_GENESIS_STATUS_COMMAND => File::Spec->catfile( $bin, 'genesis-status' ), + XCAT_GENESIS_UNAME_M => 'x86_64', + XCAT_GENESIS_UNAME_R => '6.18.24-test', + XCAT_TEST_LOG => $command_log, + XCAT_STATUS_LOG => $status_log, +); + +my ( $verify_status, $verify_output ) = + run_command( '/bin/bash', $loader, 'verify', $manifest, $image, $signature ); +is( $verify_status, 0, 'valid extension is accepted' ) or diag($verify_output); + +{ + local $ENV{XCAT_GENESIS_UNAME_M} = 'i586'; + my $x86_manifest = File::Spec->catfile( $root, 'xcat-smoke-x86.json' ); + my $x86_signature = File::Spec->catfile( $root, 'xcat-smoke-x86.sig' ); + write_manifest( $x86_manifest, $hash, { architecture => 'x86' } ); + my ( $x86_sign_status, $x86_sign_output ) = + run_command( $signer, $x86_manifest, $private_key, $x86_signature ); + is( $x86_sign_status, 0, 'x86 extension manifest is signed' ) + or diag($x86_sign_output); + my ( $x86_status, $x86_output ) = run_command( + '/bin/bash', $loader, 'verify', $x86_manifest, $image, $x86_signature + ); + is( $x86_status, 0, 'i586 runtime uses the x86 extension identity' ) + or diag($x86_output); +} + +{ + local $ENV{XCAT_GENESIS_UNAME_M} = 'armv7l'; + my $arm_manifest = File::Spec->catfile( $root, 'xcat-smoke-armv7hf.json' ); + my $arm_signature = File::Spec->catfile( $root, 'xcat-smoke-armv7hf.sig' ); + write_manifest( $arm_manifest, $hash, { architecture => 'armv7hf' } ); + my ( $arm_sign_status, $arm_sign_output ) = + run_command( $signer, $arm_manifest, $private_key, $arm_signature ); + is( $arm_sign_status, 0, 'armv7hf extension manifest is signed' ) + or diag($arm_sign_output); + my ( $arm_status, $arm_output ) = run_command( + '/bin/bash', $loader, 'verify', $arm_manifest, $image, $arm_signature + ); + is( $arm_status, 0, 'armv7l runtime uses the armv7hf extension identity' ) + or diag($arm_output); +} + +my ( $install_status, $install_output ) = + run_command( '/bin/bash', $loader, 'install', $manifest, $image, $signature ); +is( $install_status, 0, 'valid extension is installed' ) or diag($install_output); +ok( -f File::Spec->catfile( $run_dir, 'xcat-smoke.raw' ), + 'installed image uses the manifest name' ); +open( my $log_fh, '<', $command_log ) or die "Unable to read $command_log: $!"; +my $log = do { local $/; <$log_fh> }; +close($log_fh); +like( $log, qr/^systemd-sysext refresh$/m, + 'installation refreshes system extensions' ); + +my ( $load_status, $load_output ) = + run_command( '/bin/bash', $loader, 'load-all', $root ); +is( $load_status, 0, 'valid extension directory is loaded' ) + or diag($load_output); +open( my $status_fh, '<', $status_log ) + or die "Unable to read $status_log: $!"; +my $status_events = do { local $/; <$status_fh> }; +close($status_fh); +like( $status_events, + qr/^extensions RUNNING Verifying Genesis extensions$/m, + 'extension loading publishes its active state' ); +like( $status_events, + qr/^extensions READY Genesis extensions loaded$/m, + 'extension loading publishes its ready state' ); + +my $bad_signature = File::Spec->catfile( $root, 'bad.sig' ); +write_file( $bad_signature, 'x' x 64 ); +my ( $bad_signature_status, undef ) = + run_command( '/bin/bash', $loader, 'verify', $manifest, $image, $bad_signature ); +isnt( $bad_signature_status, 0, 'invalid signature is rejected' ); + +for my $case ( + [ architecture => 'riscv64', 'wrong architecture is rejected' ], + [ genesis_release => '9.9', 'wrong release is rejected' ], + [ sha256 => ( '0' x 64 ), 'wrong digest is rejected' ], + [ kernel_modules => JSON::PP::true, 'wrong kernel ABI is rejected', + kernel_release => '0.0-wrong' ], + ) +{ + my ( $field, $value, $label, @extra ) = @{$case}; + my %changes = ( $field => $value, @extra ); + my $case_manifest = File::Spec->catfile( $root, "$field.json" ); + my $case_signature = File::Spec->catfile( $root, "$field.sig" ); + write_manifest( $case_manifest, $hash, \%changes ); + my ( $case_sign_status, $case_sign_output ) = + run_command( $signer, $case_manifest, $private_key, $case_signature ); + is( $case_sign_status, 0, "$field manifest is signed" ) + or diag($case_sign_output); + my ( $case_status, undef ) = + run_command( '/bin/bash', $loader, 'verify', $case_manifest, $image, + $case_signature ); + isnt( $case_status, 0, $label ); +} + +my $linked_manifest = File::Spec->catfile( $root, 'linked.json' ); +symlink( $manifest, $linked_manifest ) or die "Unable to link $linked_manifest: $!"; +my ( $linked_status, undef ) = + run_command( '/bin/bash', $loader, 'verify', $linked_manifest, $image, + $signature ); +isnt( $linked_status, 0, 'linked manifest is rejected' ); + +my $empty_dir = File::Spec->catdir( $root, 'empty' ); +make_path($empty_dir); +write_file( $status_log, '' ); +my ( $empty_status, undef ) = + run_command( '/bin/bash', $loader, 'load-all', $empty_dir ); +isnt( $empty_status, 0, 'empty extension directory is rejected' ); +open( $status_fh, '<', $status_log ) + or die "Unable to read $status_log: $!"; +$status_events = do { local $/; <$status_fh> }; +close($status_fh); +like( $status_events, + qr/^extensions FAILED no extension manifests found in:/m, + 'extension loading publishes verification failures' ); +like( $status_events, + qr/CODE=EXTENSION_VERIFICATION_FAILED .*RECOVERY=Check extension images, manifests, signatures, and trusted keys/m, + 'extension failures include structured recovery data' ); + +done_testing();