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{^(\w+)\s*>$} ) {
+ my $closed = pop @open_sections;
+ die "Unbalanced $1> 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();