diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index 32c2d16..2f5de6c 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -28,6 +28,8 @@ jobs: - debian-13 - centos-stream-9 - centos-stream-10 + - opensuse-leap-16 + - sles-16 steps: - name: Checkout repository @@ -129,10 +131,12 @@ jobs: docker exec "$CONTAINER" journalctl -n 50 2>/dev/null || true echo "=== Apache error log ===" && \ docker exec "$CONTAINER" cat /var/log/apache2/error.log 2>/dev/null || \ - docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || true + docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || \ + docker exec "$CONTAINER" cat /var/log/apache2/error_log 2>/dev/null || true echo "=== MySQL error log (last 30 lines) ===" && \ docker exec "$CONTAINER" tail -30 /var/log/mysql/error.log 2>/dev/null || \ - docker exec "$CONTAINER" tail -30 /var/log/mysqld.log 2>/dev/null || true + docker exec "$CONTAINER" tail -30 /var/log/mysqld.log 2>/dev/null || \ + docker exec "$CONTAINER" tail -30 /var/log/mysql/mysqld.log 2>/dev/null || true echo "=== /root/passwords.txt ===" && \ docker exec "$CONTAINER" cat /root/passwords.txt 2>/dev/null || true diff --git a/README.md b/README.md index 2b54957..58e1486 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,37 @@ *is* covered by CI below, but RHEL itself isn't independently tested: Red Hat's official container images require a paid subscription, and the free UBI images can't substitute because `firewalld` and `sendmail`, both of which the script installs, aren't published to any repo UBI can reach without one. +- openSUSE Leap 16.0 + - Leap 16.0 ships PHP 8.4 natively; a real MySQL Community Server RPM (built for SLES 15) installs and runs on it + without issue - see `setup_suse`/`uninstall_suse` in `simplerisk-setup.sh`. +- SUSE Linux Enterprise Server (SLES) 16.0 + - SLES 16.0 and openSUSE Leap 16.0 share the exact same package builds (`setup_suse`/`uninstall_suse` is one code path + for both), and unlike RHEL, SLES 16 *is* independently tested: SUSE's free, unauthenticated BCI (Base Container + Image) - `registry.suse.com/bci/bci-base:16.0` - carries its own public `SLE_BCI` repo with no SCC subscription + needed, so CI runs the real installer against real SLES 16, not just its openSUSE proxy. -SUSE Linux Enterprise Server (SLES) is not currently supported: SimpleRisk requires PHP >= 8.3, and SLES 15's own -repositories only offer PHP 8.2 with no upgrade path currently available. Support may return once a SLES release with a -newer PHP is available. +## Explicitly unsupported versions + +A few versions are excluded on purpose, not simply because they haven't been tried yet: + +- **openSUSE Tumbleweed** - a rolling release, not a stable/LTS-equivalent target. Even though it always carries a + current PHP, this script intentionally only targets openSUSE's stable branch (Leap). +- **openSUSE Leap 15.x and earlier** - capped at PHP 8.2 (the same `php8` package SLES 15 ships) with no upgrade path, + and Leap 15.6, the last 15.x release, is now end-of-life. +- **SUSE Linux Enterprise Server (SLES) 15** (all service packs) - not supported, and won't be added without a way to + verify it. SLES 15 SP7's release notes list PHP 8.3.x as available, which would clear SimpleRisk's PHP >= 8.3 + requirement, but that can't currently be confirmed: openSUSE Leap 15.x, which would otherwise serve as SLES 15's + free/testable proxy the way Leap 16.0 does for SLES 16.0, ended at 15.6 (see above) and never exceeded PHP 8.2, and + real SLES 15 container images require a paid SCC subscription to test directly. +- **Ubuntu releases older than 22.04** (18.04, 20.04, etc.), and interim (non-LTS) releases - see the LTS-only policy + above. +- **Debian releases older than 13** (11, 12, etc.) - out of scope; only the version listed above is targeted. +- **CentOS Stream / RHEL releases older than 9** (8, 7, etc.) - out of scope; CentOS 8 reached end-of-life in December + 2021, and older major versions aren't targeted regardless of a given release's own support status. +- **RHEL-compatible rebuilds** (Rocky Linux, AlmaLinux, Oracle Linux, etc.) - not tested, even though they're binary- + compatible with RHEL: `/etc/os-release`'s `NAME` differs from `Red Hat Enterprise Linux`/`Red Hat Enterprise Linux + Server`, so `validate_os_and_version()` doesn't recognize them and the script exits rather than assuming + compatibility. ## Instructions diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 228ebed..ade3f3c 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -8,7 +8,7 @@ readonly CENTOS_STREAM_OSVAR='CentOS Stream' readonly RHEL_OSVAR='Red Hat Enterprise Linux' readonly RHELS_OSVAR='Red Hat Enterprise Linux Server' readonly SLES_OSVAR='SLES' -readonly SLES_15_SUPPORTED_SP="15" +readonly OPENSUSE_LEAP_OSVAR='openSUSE Leap' readonly MYSQL_KEY_URL='https://repo.mysql.com/RPM-GPG-KEY-mysql-2025' readonly MYSQL_GPG_KEY='B7B3B788A8D3785C' # Key taken from https://dev.mysql.com/doc/refman/8.4/en/checking-gpg-signature.html @@ -132,6 +132,7 @@ load_os_variables(){ validate_os_and_version(){ local valid + local untested_note case "${OS}" in "${UBUNTU_OSVAR}") # LTS releases only - interim (non-LTS) releases like 25.04/25.10 @@ -155,30 +156,50 @@ validate_os_and_version(){ if [[ "${VER}" = 9* ]] || [[ "${VER}" = 10* ]]; then valid=y SETUP_TYPE=rhel + # RHEL shares the exact same install code path as CentOS + # Stream (which CI does cover), but RHEL itself has never + # actually been run in CI: Red Hat's official container + # images require a paid subscription, and the free UBI + # images can't substitute (see README). Let the user know + # this specific OS is unverified, not just "supported". + untested_note="RHEL is not independently tested in CI (it shares its install code with CentOS Stream, which is tested) - see the README for why." fi;; "${SLES_OSVAR}") - # SLES 15 (all service packs) is not supported: SimpleRisk's - # current release requires PHP >= 8.3 (Composer platform check), - # and SLES 15's own repositories cap out at PHP 8.2 (the php8 - # package) with no upgrade path. openSUSE's community - # devel:languages:php OBS project, which sometimes backports a - # newer PHP to older releases, has dropped 15.6 support entirely - # and only targets the next major release (16.0), which is not - # yet generally available for SLES. openSUSE Leap 16.0 already - # ships PHP 8.4 natively, so SLES 16 (once released) should be a - # viable target - but setup_suse()/uninstall_suse() below are - # written entirely around SLES 15's package names, module - # structure, and MySQL repo RPM naming - # (mysql84-community-release-sl15), so adding SLES 16 support - # needs its own dedicated pass, not just changing this version - # check. - print_error_message "SLES/openSUSE is not currently supported: SimpleRisk requires PHP >= 8.3, and SLES 15's repositories only offer PHP 8.2 with no upgrade path currently available.";; + # SLES 15 (all service packs, including SP7's PHP 8.3) is not + # supported here: even though SP7's release notes list PHP 8.3.x, + # there's no way to verify that install path - openSUSE Leap + # 15.x (which would normally serve as SLES 15's free/testable + # proxy) ended at 15.6, which is now EOL and still capped at PHP + # 8.2, and real SLES 15 containers require a paid SCC + # subscription. See the README's SLES caveat for the same + # reasoning in more detail. + # + # SLES 16.0 (see below) ships PHP 8.4 and is supported and + # CI-tested directly. + if [[ "${VER}" = 16.* ]]; then + valid=y + SETUP_TYPE=suse + fi;; + "${OPENSUSE_LEAP_OSVAR}") + # Leap 16.0 ships PHP 8.4 (php8 package) and a real MySQL + # Community Server RPM (built for SLES 15) installs and runs on + # it cleanly - see setup_suse()/uninstall_suse() below. Point + # releases within 16.x are expected to stay compatible, mirroring + # how Ubuntu's 24.*/26.* are accepted above. Older Leap releases + # (15.x and earlier) cap out at PHP 8.2, same as SLES 15. + if [[ "${VER}" = 16.* ]]; then + valid=y + SETUP_TYPE=suse + fi;; *) local unknown=y;; esac if [ -n "${valid:-}" ]; then echo "Detected OS is ${OS} ${VER}, which is supported by this script." + if [ -n "${untested_note:-}" ]; then + echo "NOTE: ${untested_note}" + fi elif [ -z "${valid:-}" ] && [ ! -v unknown ]; then echo "Detected OS is ${OS} ${VER}, but this version is not currently supported by this script." exit 1 @@ -857,21 +878,18 @@ setup_suse(){ # "The Tokenizer extension is required for the routing attribute loader"), # php8-ctype, php8-xmlreader, php8-xmlwriter. Note 'php-xml' does not exist # on SUSE, and SimpleRisk's healthcheck only flags dom, so the others were - # silently missing. - run_cmd zypper -n install php8 php8-mysql apache2-mod_php8 php8-ldap php8-curl php8-zlib php8-phar php8-mbstring php8-intl php8-posix php8-gd php8-zip php8-dom php8-openssl php8-tokenizer php8-ctype php8-xmlreader php8-xmlwriter - - if [[ "${VER}" = "${SLES_15_SUPPORTED_SP}"* ]]; then - print_status 'Enabling PHP and Apache modules...' - for module in php8 rewrite ssl mod_ssl; do - run_cmd a2enmod "$module" - done - fi - - print_status 'Enabling Rewrite Module for Apache...' - if [[ "${VER}" = "${SLES_15_SUPPORTED_SP}"* ]]; then - grep -qF 'mod_rewrite.so' /etc/apache2/loadmodule.conf 2>/dev/null || \ - echo 'LoadModule rewrite_module /usr/lib64/apache2-prefork/mod_rewrite.so' >> /etc/apache2/loadmodule.conf - fi + # silently missing. php8-cli is also separate here (unlike Debian/RHEL, + # where the base PHP package includes the CLI binary) and is needed for + # the backup cron job, which invokes `php` directly. + run_cmd zypper -n install php8 php8-cli php8-mysql apache2-mod_php8 php8-ldap php8-curl php8-zlib php8-phar php8-mbstring php8-intl php8-posix php8-gd php8-zip php8-dom php8-openssl php8-tokenizer php8-ctype php8-xmlreader php8-xmlwriter + + print_status 'Enabling PHP and Apache modules...' + # a2enmod is idempotent and self-appends the needed LoadModule line, so + # this runs unconditionally rather than only on modules that aren't + # already enabled by default (e.g. php8/ssl on Leap 16.0). + for module in php8 rewrite ssl mod_ssl; do + run_cmd a2enmod "$module" + done print_status 'Setting up SimpleRisk Virtual Host and SSL Self-Signed Cert' grep -qxF 'Listen 443' /etc/apache2/vhosts.d/simplerisk.conf 2>/dev/null || echo 'Listen 443' >> /etc/apache2/vhosts.d/simplerisk.conf diff --git a/tests/dockerfiles/Dockerfile.opensuse-leap-16 b/tests/dockerfiles/Dockerfile.opensuse-leap-16 new file mode 100644 index 0000000..1ee6101 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.opensuse-leap-16 @@ -0,0 +1,33 @@ +FROM opensuse/leap:16.0 + +ENV container=docker + +# wget and which are pre-installed on a real openSUSE Leap server but absent +# from the minimal container image; simplerisk-setup.sh relies on both +# (wget to fetch the SimpleRisk bundle/database, which to locate the php +# binary for the backup cron entry) without installing them itself, since a +# real server is assumed to already have them. +RUN zypper --non-interactive install wget which && \ + zypper clean --all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable` calls work by managing +# processes directly instead. +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY systemctl-shim-suse.sh /usr/local/bin/systemctl +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd needs a no-op shim: firewalld cannot run in Docker Desktop (no +# nftables/iptables backend). Placing it in /usr/local/bin/ ensures it takes +# precedence over the real binary in /usr/sbin/ when the setup script's PATH +# is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.sles-16 b/tests/dockerfiles/Dockerfile.sles-16 new file mode 100644 index 0000000..d13a59e --- /dev/null +++ b/tests/dockerfiles/Dockerfile.sles-16 @@ -0,0 +1,35 @@ +FROM registry.suse.com/bci/bci-base:16.0 + +ENV container=docker + +# SUSE's BCI (Base Container Image) ships with its own free, unauthenticated +# `SLE_BCI` repo baked in - no SCC subscription/registration needed, unlike +# SLES 15-era images. It carries the exact same package builds as openSUSE +# Leap 16.0 (identical `160000.x.x` release strings for php8/apache2/etc.), +# just from a more minimal base: gawk, wget, and which aren't preinstalled +# here the way they are on Leap 16.0's base image, but would be on a real +# (non-container) SLES 16 server, so they're added here to match. gawk in +# particular is required by simplerisk-setup.sh's own use of `awk`. +RUN zypper --non-interactive install wget which gawk && \ + zypper clean --all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable` calls work by managing +# processes directly instead. Shared with the openSUSE Leap 16.0 image since +# setup_suse()/uninstall_suse() is the same code path for both. +COPY systemctl-shim-suse.sh /usr/local/bin/systemctl +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd needs a no-op shim: firewalld cannot run in Docker Desktop (no +# nftables/iptables backend). Placing it in /usr/local/bin/ ensures it takes +# precedence over the real binary in /usr/sbin/ when the setup script's PATH +# is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/systemctl-shim-suse.sh b/tests/dockerfiles/systemctl-shim-suse.sh new file mode 100755 index 0000000..3f1fd9c --- /dev/null +++ b/tests/dockerfiles/systemctl-shim-suse.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh +# on openSUSE Leap without requiring a running systemd PID 1. + +now_flag= +args=() +for arg in "$@"; do + if [[ "$arg" == "--now" ]]; then + now_flag=1 + continue + fi + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql + # Pre-create the log file with mysql ownership so mysqld (which drops to + # the mysql user before opening it) can write to it - a bare shell + # redirect below would otherwise create it root-owned and unwritable. + touch /var/log/mysql/mysqld.log && chown mysql:mysql /var/log/mysql/mysqld.log + # mysqld_pre_systemd is the same helper the real mysql.service unit runs + # as ExecStartPre; it's idempotent (skips init if the datadir is already + # populated) and internally runs `mysqld --initialize`, which is meant to + # write the temp root password as a [Note] line to /var/log/mysql/mysqld.log + # (per log-error in /etc/my.cnf) for simplerisk-setup.sh to read - but on + # some hosts (observed on GitHub Actions runners, not reproducible in local + # Docker Desktop testing) that Note only reaches mysqld_pre_systemd's own + # stdout/stderr, never the file. Redirect explicitly into the log file so + # the Note lands there regardless, matching the CentOS shim's same + # defensive redirect around its own `mysqld --initialize` call. + /usr/bin/mysqld_pre_systemd >>/var/log/mysql/mysqld.log 2>&1 + nohup /usr/sbin/mysqld --user=mysql >>/var/log/mysql/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + # Send SIGTERM directly to the mysqld process so that no root password is + # needed (mysqladmin shutdown requires auth after setup changes the password). + local pidfile=/var/run/mysql/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_cron() { + pgrep -x cron >/dev/null 2>&1 && return 0 + nohup /usr/sbin/cron -n >/dev/null 2>&1 & +} + +case "$action" in + start) + case "$unit" in + mysql|mysqld) start_mysqld ;; + apache2) start_apache2 -k start ;; + cron) start_cron ;; + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysql|mysqld) stop_mysqld ;; + apache2) start_apache2 -k stop ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysql|mysqld) stop_mysqld; sleep 1; start_mysqld ;; + # `systemctl restart` maps to a real stop+start (ExecStop then + # ExecStart), not the `-k graceful` rolling reload systemd uses + # for `systemctl reload` - a graceful reload here reliably + # segfaults newly-forked children (observed repeatedly in + # testing), while a full stop/start does not. + apache2) start_apache2 -k stop; sleep 1; start_apache2 -k start ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 ;; + apache2) pgrep -x httpd-prefork >/dev/null 2>&1 ;; + cron) pgrep -x cron >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + apache2) pgrep -x httpd-prefork >/dev/null 2>&1 && echo "active" || exit 3 ;; + cron) pgrep -x cron >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable) + # We don't manage boot-time units, but `enable --now` also means + # "start it now" on a real system - honor the --now part. + if [ -n "$now_flag" ]; then + case "$unit" in + mysql|mysqld) start_mysqld ;; + apache2) start_apache2 -k start ;; + cron) start_cron ;; + *) exit 0 ;; + esac + else + exit 0 + fi ;; + disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + exit 0 ;; # no-op — we don't manage boot-time units + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac diff --git a/tests/verify-install.sh b/tests/verify-install.sh index e463184..6c23ccb 100755 --- a/tests/verify-install.sh +++ b/tests/verify-install.sh @@ -112,7 +112,7 @@ check "PHP CLI is functional" php -r "echo 'OK';" check "PHP version is 8.x" bash -c "php --version | grep -qE '^PHP 8\.'" for ext in mysqli mbstring xml curl gd zip intl ldap; do - check "PHP extension '$ext' is loaded" php -m | grep -qi "$ext" + check "PHP extension '$ext' is loaded" bash -c "php -m | grep -qi '$ext'" done # ── Web server (OS-conditional) ───────────────────────────────────────────────