2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 23:28:19 +00:00

Merge pull request #7899 from VersatusHPC/fix/xcat-ws-conf-module-path

fix(xCAT-server): xcat-ws.conf names a SUSE module path and never matches its package
This commit is contained in:
Daniel Hilst
2026-10-06 10:21:57 -03:00
committed by GitHub
9 changed files with 622 additions and 59 deletions
+36
View File
@@ -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();
+12 -32
View File
@@ -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
+28
View File
@@ -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.
<IfModule mod_rewrite.c>
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]
</IfModule>
# Without the redirect, deny the REST aliases.
<IfModule !mod_rewrite.c>
RedirectMatch 403 ^/xcatws(v2)?(/|$)
</IfModule>
<FilesMatch "^(xcatws.cgi|zvmxcatws.cgi)$">
# Both spellings, so one file serves Apache 2.2 and 2.4.
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Allow from all
</IfModule>
</FilesMatch>
@@ -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]
<FilesMatch "^(xcatws.cgi|zvmxcatws.cgi)$">
Order allow,deny
Allow from all
</FilesMatch>
@@ -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]
<FilesMatch "^(xcatws.cgi|zvmxcatws.cgi)$">
Require all granted
</FilesMatch>
@@ -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
+3 -2
View File
@@ -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
+366
View File
@@ -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
<Directory />
AllowOverride None
Require all denied
</Directory>
<Directory $dir/htdocs>
Require all granted
</Directory>
Alias /install $dir/install
<Directory $dir/install>
Require all granted
</Directory>
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();
+176
View File
@@ -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 <IfModule mod_rewrite.c>" );
}
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 <IfModule mod_authz_core.c>" );
}
foreach my $directive (@legacy) {
ok( guarded_by( $directive, 'IfModule !mod_authz_core.c' ),
"$fragment guards $directive->{directive} with <IfModule !mod_authz_core.c>" );
}
}
#-----------------------------------------------------------------------------
# %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();