diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index 847290e14..74ef58b35 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -498,6 +498,34 @@ sub run_bats_tests{ return 0; } +#-------------------------------------------------------- +# Fuction name: run_source_integration_tests +# Description: Run the integration tests that read a file the Debian +# packages do not install. They run against the pre-build copy +# of the source tree, after install_xcat() put Apache on the +# runner. A skip fails the phase: the runner has every +# prerequisite. +# Attributes: +# Return code: 0 all tests passed, 1 otherwise +#-------------------------------------------------------- +sub run_source_integration_tests{ + my $cmd = "cd $unitsrc && prove -v xCAT-test/integration/wsapi_apache_access.t"; + print "[run_source_integration_tests] running $cmd\n"; + my @output = runcmd("$cmd"); + print Dumper \@output; + if($::RUNCMD_RC or grep { /skipped/i } @output){ + print RED "[run_source_integration_tests] $cmd ....[Failed]\n"; + $check_result_str .= "> **SOURCE INTEGRATION TESTS Failed** : Please click ``Details`` label in ``Merge pull request`` box for detailed information\n"; + print $check_result_str; + return 1; + } + + print "[run_source_integration_tests] $cmd ....[Pass]\n"; + $check_result_str .= "> **SOURCE INTEGRATION TESTS Successful**\n"; + print $check_result_str; + return 0; +} + #-------------------------------------------------------- # Fuction name: run_blackboxtest_unit_tests # Description: Run the blackboxtest offline checks: its Python unit tests and @@ -1017,6 +1045,14 @@ if($rst){ } mark_time("run_bats_tests"); +print GREEN "\n------Running xCAT-test source integration tests ------\n"; +$rst = run_source_integration_tests(); +if($rst){ + print RED "Run of xCAT-test source integration tests failed\n"; + exit $rst; +} +mark_time("run_source_integration_tests"); + #Check the syntax of changing code print GREEN "\n------ Checking the syntax of changed code------\n"; $rst = check_syntax(); diff --git a/xCAT-server/xCAT-server.spec b/xCAT-server/xCAT-server.spec index 36c2941de..f8cf2766d 100644 --- a/xCAT-server/xCAT-server.spec +++ b/xCAT-server/xCAT-server.spec @@ -418,32 +418,26 @@ rm -f $RPM_BUILD_ROOT/%{prefix}/ws/zvmxcatws.cgi %if %fsm %else -echo "ScriptAlias /xcatrhevh %{prefix}/ws/xcatrhevh.cgi" > $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 -echo "ScriptAlias /xcatrhevh %{prefix}/ws/xcatrhevh.cgi" > $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 +echo "ScriptAlias /xcatrhevh %{prefix}/ws/xcatrhevh.cgi" > $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf %if %notpcm %if %nots390x -echo "ScriptAlias /xcatws %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 -echo "ScriptAlias /xcatws %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 +echo "ScriptAlias /xcatws %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf %else # Add in old version 1 REST-API and version 2 REST-API to z/VM, default to version 1 -echo "ScriptAlias /xcatwsv2 %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 -echo "ScriptAlias /xcatwsv2 %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 -echo "ScriptAlias /xcatws %{prefix}/ws/zvmxcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 -echo "ScriptAlias /xcatws %{prefix}/ws/zvmxcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 +echo "ScriptAlias /xcatwsv2 %{prefix}/ws/xcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf +echo "ScriptAlias /xcatws %{prefix}/ws/zvmxcatws.cgi" >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf %endif %endif -cat $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf.apache22 >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 -cat $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf.apache24 >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 -#install lower version(<2.4) apache/httpd conf files by default -cp $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat-ws.conf -cp $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache22 $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat-ws.conf +cat $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf >> $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf +# One fragment serves Apache 2.2 and 2.4, so the payload is also the final file. +cp $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat-ws.conf +cp $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat-ws.conf $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat-ws.conf %endif -rm -f $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf.apache22 -rm -f $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf.apache24 +rm -f $RPM_BUILD_ROOT/%{prefix}/ws/xcat-ws.conf %clean rm -rf $RPM_BUILD_ROOT @@ -607,23 +601,9 @@ if [ "$1" -gt "1" ]; then #only on upgrade for AIX... fi %endif -#Apply the correct httpd/apache configuration file according to the httpd/apache version -if [ -n "$(httpd -v 2>&1 |grep -e '^Server version\s*:.*/2\.4')" ] -then - rm -rf /etc/httpd/conf.d/xcat-ws.conf - cp /etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 /etc/httpd/conf.d/xcat-ws.conf -fi - -if [ -n "$(apachectl -v 2>&1 |grep -e '^Server version\s*:.*/2\.4')" ] -then - rm -rf /etc/apache2/conf.d/xcat-ws.conf - cp /etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 /etc/apache2/conf.d/xcat-ws.conf -fi - -if [ -n "$(apache2ctl -v 2>&1 |grep -e '^Server version\s*:.*/2\.4')" ] -then - rm -rf /etc/apache2/conf.d/xcat-ws.conf - cp /etc/%httpconfigdir/conf.orig/xcat-ws.conf.apache24 /etc/apache2/conf.d/xcat-ws.conf +# SUSE does not load mod_rewrite by default. A no-op where a2enmod is absent. +if command -v a2enmod >/dev/null 2>&1; then + a2enmod rewrite >/dev/null 2>&1 || : fi exit 0 diff --git a/xCAT-server/xCAT-wsapi/xcat-ws.conf b/xCAT-server/xCAT-wsapi/xcat-ws.conf new file mode 100644 index 000000000..64cc7612a --- /dev/null +++ b/xCAT-server/xCAT-wsapi/xcat-ws.conf @@ -0,0 +1,28 @@ +# The Apache configuration for the xCAT REST API, for every RPM family. +# +# Do not add a LoadModule: it names one distribution's module directory and is +# then absent on the others, which stops Apache from starting. + +# Redirect http to https. Guarded because mod_rewrite can be absent. + +RewriteEngine On +RewriteCond %{SERVER_PORT} 80 +RewriteCond %{HTTPS} !=on +RewriteRule ^/?xcatws/(.*) https://%{SERVER_NAME}/xcatws/$1 [R,L] +RewriteRule ^/?xcatwsv2/(.*) https://%{SERVER_NAME}/xcatwsv2/$1 [R,L] + +# Without the redirect, deny the REST aliases. + +RedirectMatch 403 ^/xcatws(v2)?(/|$) + + + +# Both spellings, so one file serves Apache 2.2 and 2.4. + +Require all granted + + +Order allow,deny +Allow from all + + diff --git a/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache22 b/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache22 deleted file mode 100644 index a8432e979..000000000 --- a/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache22 +++ /dev/null @@ -1,14 +0,0 @@ -LoadModule rewrite_module /usr/lib64/apache2-prefork/mod_rewrite.so - -# Redirect all http request to https -RewriteEngine On -RewriteCond %{SERVER_PORT} 80 -RewriteCond %{HTTPS} !=on -RewriteRule ^/?xcatws/(.*) https://%{SERVER_NAME}/xcatws/$1 [R,L] -RewriteRule ^/?xcatwsv2/(.*) https://%{SERVER_NAME}/xcatwsv2/$1 [R,L] - - - Order allow,deny - Allow from all - - diff --git a/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache24 b/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache24 deleted file mode 100644 index 7c54e25bc..000000000 --- a/xCAT-server/xCAT-wsapi/xcat-ws.conf.apache24 +++ /dev/null @@ -1,10 +0,0 @@ -LoadModule rewrite_module /usr/lib64/apache2-prefork/mod_rewrite.so -RewriteEngine On -RewriteCond %{SERVER_PORT} 80 -RewriteCond %{HTTPS} !=on -RewriteRule ^/?xcatws/(.*) https://%{SERVER_NAME}/xcatws/$1 [R,L] -RewriteRule ^/?xcatwsv2/(.*) https://%{SERVER_NAME}/xcatwsv2/$1 [R,L] - - -Require all granted - diff --git a/xCAT-test/autotest/testcase/integration/cases0 b/xCAT-test/autotest/testcase/integration/cases0 index e81d1bd89..907e45837 100644 --- a/xCAT-test/autotest/testcase/integration/cases0 +++ b/xCAT-test/autotest/testcase/integration/cases0 @@ -4,7 +4,7 @@ os:Linux label:mn_only,ci_test,integration cmd:prove -I/opt/xcat/lib/perl -I/opt/xcat/lib/perl/xCAT -r /opt/xcat/share/xcat/tools/autotest/integration check:rc==0 -check:output=~Files=9, +check:output=~Files=10, end start:xcatd_dispatch_redaction diff --git a/xCAT-test/integration/README.md b/xCAT-test/integration/README.md index ee6f0e22c..eec8664c9 100644 --- a/xCAT-test/integration/README.md +++ b/xCAT-test/integration/README.md @@ -27,8 +27,8 @@ against it. Note the `-I` flags: unlike the unit tests these run from the installed location, so they pick up xCAT modules from `/opt/xcat/lib/perl` rather than from a source tree. -The case checks `rc==0` and `output=~Files=9,`. The file count checks that `prove` -found all nine files, which catches a packaging regression or a test renamed away from +The case checks `rc==0` and `output=~Files=10,`. The file count checks that `prove` +found all ten files, which catches a packaging regression or a test renamed away from `.t` without updating the count. It does not prove that environment-gated cases ran; inspect the TAP output for skips. **Add to that number when you add a test.** A missing directory is already caught by `rc==0` -- `prove -r` on a path that does not exist exits @@ -55,6 +55,7 @@ A test belongs in `integration/` when it needs something the checkout cannot pro | `html_form_runtime_dependency.t` | the installed `HTML::Form` module | | `openeuler_package_postscripts.t` | Linux root, private mount namespaces and DNF; tests complete package postscripts with command doubles and an isolated repository directory | | `openeuler_postscript_repositories.t` | Linux root, private mount namespaces, DNF and RPM signing tools; tests actual signed repository transactions in isolated install roots | +| `wsapi_apache_access.t` | an Apache binary and its modules; reads `xcat-ws.conf` from the source tree, so `github_action_xcat_test.pl` runs it from the checkout and the installed copy skips | | `xcatd_dispatch_redaction.sh` | a live `xcatd`, syslog delivery to `/var/log/xcat/cluster.log`, and root | Run `xcattest -t xcatd_dispatch_redaction` to check password redaction in the diff --git a/xCAT-test/integration/wsapi_apache_access.t b/xCAT-test/integration/wsapi_apache_access.t new file mode 100644 index 000000000..f0a9cf27c --- /dev/null +++ b/xCAT-test/integration/wsapi_apache_access.t @@ -0,0 +1,366 @@ +#!/usr/bin/env perl +# Starts a private Apache that includes the xcat-ws.conf fragment from the +# source tree, once without mod_rewrite and once with it, and checks what the +# REST aliases and a provisioning path answer. +use strict; +use warnings; + +use FindBin; +use File::Slurper qw(read_text); +use File::Spec; +use File::Temp qw(tempdir); +use IO::Socket::INET; +use POSIX qw(WNOHANG _exit setsid); +use Test::More; +use Time::HiRes qw(sleep); + +my $fragment = $ENV{XCAT_WS_CONF} + || File::Spec->rel2abs("$FindBin::Bin/../../xCAT-server/xCAT-wsapi/xcat-ws.conf"); +plan skip_all => "the xcat-ws.conf fragment is not at $fragment" + unless -r $fragment; + +my $httpd = find_httpd(); +plan skip_all => 'no Apache httpd binary is installed' unless $httpd; + +my $builtin = join( "\n", `$httpd -l 2>/dev/null` ); +my $moddir = find_module_dir(); +plan skip_all => 'no Apache module directory with mod_alias.so is present' + unless $moddir; + +#----------------------------------------------------------------------------- + +=head3 find_httpd + + Descriptions: Finds the Apache binary of the distribution. + Arguments: none + Returns: the path, or undef when no binary is installed. +=cut + +#----------------------------------------------------------------------------- +sub find_httpd { + foreach my $path (qw(/usr/sbin/httpd /usr/sbin/apache2 /usr/sbin/httpd-prefork)) { + return $path if -x $path; + } + return undef; +} + +#----------------------------------------------------------------------------- + +=head3 find_module_dir + + Descriptions: Finds the directory that holds the Apache shared modules. + Arguments: none + Returns: the path, or undef when no candidate holds mod_alias.so. +=cut + +#----------------------------------------------------------------------------- +sub find_module_dir { + foreach my $dir ( + qw(/usr/lib64/httpd/modules /usr/lib/httpd/modules /usr/lib/apache2/modules + /usr/lib64/apache2-prefork /usr/lib64/apache2) + ) + { + return $dir if -e "$dir/mod_alias.so"; + } + return undef; +} + +#----------------------------------------------------------------------------- + +=head3 load_module_lines + + Descriptions: Builds the LoadModule lines for the modules that are not + compiled into the binary. + Arguments: the module names without the mod_ prefix + Returns: the configuration text. +=cut + +#----------------------------------------------------------------------------- +sub load_module_lines { + my (@names) = @_; + my $text = ''; + foreach my $name (@names) { + next if $builtin =~ /\bmod_\Q$name\E\.c\b/; + die "mod_$name.so is not in $moddir" unless -e "$moddir/mod_$name.so"; + $text .= "LoadModule ${name}_module $moddir/mod_$name.so\n"; + } + return $text; +} + +#----------------------------------------------------------------------------- + +=head3 free_port + + Descriptions: Asks the kernel for a free TCP port on the loopback address. + Arguments: none + Returns: the port number. +=cut + +#----------------------------------------------------------------------------- +sub free_port { + my $sock = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => 0, + Listen => 1, + Proto => 'tcp', + ) or die "Unable to find a free port: $!"; + my $port = $sock->sockport(); + close($sock); + return $port; +} + +#----------------------------------------------------------------------------- + +=head3 write_config + + Descriptions: Writes an Apache configuration that serves a stub REST CGI + through the xCAT aliases and a provisioning file under + /install, then includes the fragment. + Arguments: the work directory, the port, true to load mod_rewrite + Returns: the path of the configuration file. +=cut + +#----------------------------------------------------------------------------- +sub write_config { + my ( $dir, $port, $with_rewrite ) = @_; + + my @modules = qw(mpm_prefork unixd authz_core alias cgi); + push @modules, 'rewrite' if $with_rewrite; + @modules = grep { $_ ne 'mpm_prefork' } @modules + if $builtin =~ /\b(prefork|worker|event)\.c\b/; + + my $user = ''; + if ( $> == 0 ) { + my $group = getgrgid( ( getpwnam('nobody') )[3] ); + $user = "User nobody\nGroup $group\n"; + } + + # UseCanonicalName gives the fragment SERVER_PORT 80 on a private port. + my $conf = load_module_lines(@modules) . <<"EOF"; +ServerRoot $dir +ServerName localhost:80 +UseCanonicalName On +Listen 127.0.0.1:$port +PidFile $dir/httpd.pid +DefaultRuntimeDir $dir +ErrorLog $dir/error_log +LogLevel warn +$user +DocumentRoot $dir/htdocs + + AllowOverride None + Require all denied + + + Require all granted + +Alias /install $dir/install + + Require all granted + +ScriptAlias /xcatws $dir/ws/xcatws.cgi +ScriptAlias /xcatwsv2 $dir/ws/xcatws.cgi +Include $fragment +EOF + + my $path = "$dir/httpd.conf"; + open( my $fh, '>', $path ) or die "Unable to write $path: $!"; + print $fh $conf; + close($fh) or die "Unable to close $path: $!"; + return $path; +} + +#----------------------------------------------------------------------------- + +=head3 make_tree + + Descriptions: Creates the document tree, the provisioning file and the + stub REST CGI under a new directory. + Arguments: none + Returns: the directory. +=cut + +#----------------------------------------------------------------------------- +sub make_tree { + my $dir = tempdir( CLEANUP => 1 ); + + # Apache drops to nobody when the test runs as root. + chmod 0755, $dir or die "Unable to chmod $dir: $!"; + foreach my $sub (qw(htdocs install ws)) { + mkdir "$dir/$sub" or die "Unable to create $dir/$sub: $!"; + } + + open( my $probe, '>', "$dir/install/probe.txt" ) or die "probe.txt: $!"; + print $probe "install-probe\n"; + close($probe); + + my $cgi = "$dir/ws/xcatws.cgi"; + open( my $fh, '>', $cgi ) or die "Unable to write $cgi: $!"; + print $fh "#!/bin/sh\nprintf 'Content-Type: text/plain\\n\\nxcatws-stub\\n'\n"; + close($fh); + chmod 0755, $cgi or die "Unable to chmod $cgi: $!"; + return $dir; +} + +#----------------------------------------------------------------------------- + +=head3 start_httpd + + Descriptions: Starts Apache in the foreground and waits for its port. + Arguments: the configuration file, the port, the work directory + Returns: the process id. +=cut + +#----------------------------------------------------------------------------- +sub start_httpd { + my ( $conf, $port, $dir ) = @_; + + my $pid = fork(); + die "Unable to fork: $!" unless defined $pid; + if ( $pid == 0 ) { + + # prefork signals its whole process group when it stops. + setsid(); + open( STDIN, '<', '/dev/null' ); + open( STDOUT, '>', "$dir/stdout" ); + open( STDERR, '>&', \*STDOUT ); + { exec( $httpd, '-f', $conf, '-DFOREGROUND' ) }; + _exit(127); + } + + foreach ( 1 .. 100 ) { + if ( waitpid( $pid, WNOHANG ) == $pid ) { + diag( read_log("$dir/stdout") . read_log("$dir/error_log") ); + die "Apache exited with status $?"; + } + my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" ); + return $pid if $sock; + sleep 0.1; + } + stop_httpd($pid); + die "Apache did not listen on port $port"; +} + +#----------------------------------------------------------------------------- + +=head3 stop_httpd + + Descriptions: Stops the Apache parent process and reaps it. + Arguments: the process id + Returns: nothing. +=cut + +#----------------------------------------------------------------------------- +sub stop_httpd { + my ($pid) = @_; + kill 'TERM', $pid; + waitpid( $pid, 0 ); + return; +} + +#----------------------------------------------------------------------------- + +=head3 read_log + + Descriptions: Reads an Apache log for a diagnostic message. + Arguments: the path + Returns: the contents, or an empty string when the file is absent. +=cut + +#----------------------------------------------------------------------------- +sub read_log { + my ($path) = @_; + return -e $path ? read_text($path) : ''; +} + +#----------------------------------------------------------------------------- + +=head3 get + + Descriptions: Sends one GET request to the private Apache. + Arguments: the port, the request path + Returns: the status code, the Location header and the body. +=cut + +#----------------------------------------------------------------------------- +sub get { + my ( $port, $path ) = @_; + my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port", Timeout => 10 ) + or die "Unable to connect to port $port: $!"; + print $sock "GET $path HTTP/1.0\r\nHost: localhost\r\n\r\n"; + local $/; + my $response = <$sock>; + close($sock); + + my ($status) = $response =~ m{^HTTP/\S+\s+(\d+)}; + my ($location) = $response =~ m{^Location:\s*(\S+)}mi; + my ( undef, $body ) = split( /\r\n\r\n/, $response, 2 ); + return ( $status // 0, $location // '', $body // '' ); +} + +#----------------------------------------------------------------------------- + +=head3 with_httpd + + Descriptions: Runs a block of checks against a private Apache. + Arguments: true to load mod_rewrite, the block, which gets the port + Returns: nothing. +=cut + +#----------------------------------------------------------------------------- +sub with_httpd { + my ( $with_rewrite, $code ) = @_; + my $dir = make_tree(); + my $port = free_port(); + my $conf = write_config( $dir, $port, $with_rewrite ); + + my $syntax = `$httpd -t -f $conf 2>&1`; + is( $?, 0, ( $with_rewrite ? 'with' : 'without' ) . ' mod_rewrite the configuration parses' ) + or diag($syntax); + + my $pid = start_httpd( $conf, $port, $dir ); + my $ok = eval { $code->($port); 1 }; + my $error = $@; + stop_httpd($pid); + die $error unless $ok; + return; +} + +my @rest_paths = ( '/xcatws/version', '/xcatwsv2/version', '/xcatws', '/xcatwsv2' ); + +subtest 'without mod_rewrite' => sub { + with_httpd( + 0, + sub { + my ($port) = @_; + foreach my $path (@rest_paths) { + my ( $status, undef, $body ) = get( $port, $path ); + is( $status, 403, "$path answers 403" ); + unlike( $body, qr/xcatws-stub/, "$path does not run the REST CGI" ); + } + my ( $status, undef, $body ) = get( $port, '/install/probe.txt' ); + is( $status, 200, '/install is served' ); + like( $body, qr/install-probe/, '/install returns the file' ); + } + ); +}; + +subtest 'with mod_rewrite' => sub { + with_httpd( + 1, + sub { + my ($port) = @_; + foreach my $alias (qw(xcatws xcatwsv2)) { + my ( $status, $location ) = get( $port, "/$alias/version" ); + is( $status, 302, "/$alias/version answers 302" ); + is( $location, "https://localhost/$alias/version", + "/$alias/version redirects to https" ); + } + my ( $status, undef, $body ) = get( $port, '/install/probe.txt' ); + is( $status, 200, '/install is served' ); + like( $body, qr/install-probe/, '/install returns the file' ); + } + ); +}; + +done_testing(); diff --git a/xCAT-test/unit/wsapi_apache_conf_portable.t b/xCAT-test/unit/wsapi_apache_conf_portable.t new file mode 100644 index 000000000..2e2587047 --- /dev/null +++ b/xCAT-test/unit/wsapi_apache_conf_portable.t @@ -0,0 +1,176 @@ +#!/usr/bin/env perl +# The fragment and the spec do not execute and their text IS the contract, so this +# test parses them rather than running them. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path slurp_repo_file); + +my $wsapi_dir = 'xCAT-server/xCAT-wsapi'; + +#----------------------------------------------------------------------------- + +=head3 parse_apache_fragment + + Descriptions: Parses an Apache configuration fragment. + Arguments: the fragment contents + Returns: a list of hash references, one per directive, each with the + directive name, its arguments and the enclosing sections from the + outside in. A section is recorded as its name plus its argument, + for example "IfModule mod_rewrite.c". +=cut + +#----------------------------------------------------------------------------- +sub parse_apache_fragment { + my ($contents) = @_; + my (@directives, @open_sections); + + foreach my $line ( split( /\n/, $contents ) ) { + my $text = $line; + $text =~ s/^\s+//; + $text =~ s/\s+$//; + next unless length $text; + next if $text =~ /^#/; + + if ( $text =~ m{^$} ) { + my $closed = pop @open_sections; + die "Unbalanced in an Apache fragment" unless defined $closed; + next; + } + if ( $text =~ m{^<(\w+)\s*(.*?)\s*>$} ) { + my ( $name, $argument ) = ( $1, $2 ); + $argument =~ s/^"(.*)"$/$1/; + push @open_sections, + length($argument) ? "$name $argument" : $name; + next; + } + + my ( $directive, @arguments ) = split( /\s+/, $text ); + push @directives, + { + directive => $directive, + arguments => [@arguments], + sections => [@open_sections], + }; + } + + die 'An Apache fragment left a section open' if @open_sections; + return @directives; +} + +#----------------------------------------------------------------------------- + +=head3 parse_spec_sections + + Descriptions: Splits an RPM spec into its sections. + Arguments: the spec contents + Returns: a hash reference keyed on the section name without the leading + percent sign, each value the lines of that section. +=cut + +#----------------------------------------------------------------------------- +sub parse_spec_sections { + my ($contents) = @_; + my %section; + my $current = 'preamble'; + + foreach my $line ( split( /\n/, $contents ) ) { + if ( $line =~ /^%(build|install|clean|files|pre|post|preun|postun|posttrans|changelog|description|package)\b/ ) { + $current = $1; + $section{$current} ||= []; + next; + } + push @{ $section{$current} ||= [] }, $line; + } + + return \%section; +} + +sub guarded_by { + my ( $directive, $section ) = @_; + return scalar grep { $_ eq $section } @{ $directive->{sections} }; +} + +#----------------------------------------------------------------------------- +# The RPM families only. debian/rules owns xcat-ws.conf.ubuntu. +#----------------------------------------------------------------------------- +my @fragments = sort grep { !m{\.ubuntu$} } + map { my $p = $_; $p =~ s{^.*/(xCAT-server/)}{$1}; $p } + glob( repo_path("$wsapi_dir/xcat-ws.conf*") ); + +die "No Apache fragment found under $wsapi_dir; the test can measure nothing\n" + unless @fragments; + +foreach my $fragment (@fragments) { + my @directives = parse_apache_fragment( slurp_repo_file($fragment) ); + + # A LoadModule naming one distribution's MPM directory is absent on the others. + my @load_module = grep { lc( $_->{directive} ) eq 'loadmodule' } @directives; + is( scalar @load_module, 0, + "$fragment declares no LoadModule, so the server owns module loading" ) + or diag( 'LoadModule found: ' + . join( ', ', map { join( ' ', @{ $_->{arguments} } ) } @load_module ) ); + + my @absolute = grep { + my $d = $_; + grep { m{^/} } @{ $d->{arguments} } + } @directives; + is( scalar @absolute, 0, + "$fragment names no absolute filesystem path, so no distribution layout is assumed" ) + or diag( 'absolute path in: ' + . join( ', ', map { $_->{directive} } @absolute ) ); + + # One file serves both generations, each directive behind its own guard. + my @rewrite = grep { lc( $_->{directive} ) =~ /^rewrite/ } @directives; + ok( scalar @rewrite, "$fragment still redirects http to https" ); + foreach my $directive (@rewrite) { + ok( guarded_by( $directive, 'IfModule mod_rewrite.c' ), + "$fragment guards $directive->{directive} with " ); + } + + my @modern = grep { lc( $_->{directive} ) eq 'require' } @directives; + my @legacy = grep { lc( $_->{directive} ) =~ /^(order|allow|deny)$/ } @directives; + ok( scalar @modern, "$fragment grants access in the Apache 2.4 spelling" ); + ok( scalar @legacy, "$fragment grants access in the Apache 2.2 spelling" ); + foreach my $directive (@modern) { + ok( guarded_by( $directive, 'IfModule mod_authz_core.c' ), + "$fragment guards $directive->{directive} with " ); + } + foreach my $directive (@legacy) { + ok( guarded_by( $directive, 'IfModule !mod_authz_core.c' ), + "$fragment guards $directive->{directive} with " ); + } +} + +#----------------------------------------------------------------------------- +# %post must not replace the payload, or rpm -V reports drift for the life of the install. +#----------------------------------------------------------------------------- +my $spec = parse_spec_sections( slurp_repo_file('xCAT-server/xCAT-server.spec') ); + +my @live_paths = + ( '/etc/httpd/conf.d/xcat-ws.conf', '/etc/apache2/conf.d/xcat-ws.conf' ); + +foreach my $path (@live_paths) { + my @post = grep { index( $_, $path ) >= 0 } @{ $spec->{post} }; + is( scalar @post, 0, + "%post never names $path, so it cannot replace the installed payload" ); + diag("%post line: $_") foreach @post; + + my @install = grep { /^\s*cp\s.*\Q$path\E\s*$/ } @{ $spec->{install} }; + ok( scalar @install, "%install installs $path as package payload" ); + + ok( scalar( grep { /^\Q$path\E\s*$/ } @{ $spec->{files} } ), + "%files owns $path, so rpm verifies it" ); +} + +# The LoadModule removed from the fragment is replaced by the distribution's own +# module management. On SUSE apache2 that is a2enmod, as xCAT.spec already does +# for mod_headers; on EL conf.modules.d loads mod_rewrite already. +ok( scalar( grep { /\ba2enmod\s+rewrite\b/ } @{ $spec->{post} } ), + '%post enables mod_rewrite where a2enmod manages modules' ); + +done_testing();