diff --git a/autoload.php b/autoload.php index 3ae7a2e..7a60a1c 100644 --- a/autoload.php +++ b/autoload.php @@ -52,6 +52,7 @@ require_once(__DIR__ . '/deployer/requirements/task/check_env.php'); require_once(__DIR__ . '/deployer/requirements/task/check_eol.php'); require_once(__DIR__ . '/deployer/requirements/task/list.php'); +require_once(__DIR__ . '/deployer/requirements/task/health.php'); /* * dev diff --git a/deployer/requirements/config/set.php b/deployer/requirements/config/set.php index 8f523d4..56da57d 100644 --- a/deployer/requirements/config/set.php +++ b/deployer/requirements/config/set.php @@ -18,6 +18,7 @@ set('requirements_check_env_enabled', true); set('requirements_check_eol_enabled', true); set('requirements_check_database_grants_enabled', true); +set('requirements_check_health_enabled', true); // Locales set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']); @@ -108,6 +109,9 @@ set('requirements_eol_warn_months', 6); set('requirements_eol_api_timeout', 5); +// Health check +set('requirements_health_url', 'http://localhost'); + // User / permissions set('requirements_user_group', 'www-data'); set('requirements_deploy_path_permissions', '2770'); diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 65b10eb..7fd57a1 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -352,6 +352,36 @@ function checkEolForProduct(string $label, string $product, string $cycle, int $ evaluateEolStatus("$label $cycle", $match, $warnMonths); } +/** + * Check if a service is active via systemctl, with pgrep fallback. + * + * Returns the matched service/process name or null if none found. + */ +function isServiceActive(string ...$names): ?string +{ + $hasSystemctl = test('command -v systemctl > /dev/null 2>&1'); + + foreach ($names as $name) { + try { + if ($hasSystemctl) { + $status = trim(run("systemctl is-active $name 2>/dev/null || true")); + + if ($status === 'active') { + return $name; + } + } + + if (test("pgrep -x $name > /dev/null 2>&1")) { + return $name; + } + } catch (RunException) { + continue; + } + } + + return null; +} + /** * @return array */ diff --git a/deployer/requirements/task/health.php b/deployer/requirements/task/health.php new file mode 100644 index 0000000..ba002b9 --- /dev/null +++ b/deployer/requirements/task/health.php @@ -0,0 +1,88 @@ +/dev/null')); + + if (!preg_match('/^\d+\.\d+$/', $phpVersion)) { + throw new RunException('php', 1, "Unexpected PHP version output: $phpVersion", ''); + } + + $fpmService = isServiceActive("php$phpVersion-fpm", 'php-fpm'); + + if ($fpmService !== null) { + addRequirementRow('PHP-FPM', REQUIREMENT_OK, "Active ($fpmService)"); + } else { + addRequirementRow('PHP-FPM', REQUIREMENT_FAIL, 'Process not found'); + } + } catch (RunException) { + addRequirementRow('PHP-FPM', REQUIREMENT_SKIP, 'Could not determine PHP version'); + } + + // 2. Webserver + $webserver = isServiceActive('nginx', 'apache2', 'httpd'); + + if ($webserver !== null) { + addRequirementRow('Webserver', REQUIREMENT_OK, "Active ($webserver)"); + } else { + addRequirementRow('Webserver', REQUIREMENT_FAIL, 'No nginx, apache2 or httpd process found'); + } + + // 3. Database server + $db = detectDatabaseProduct(); + $dbLabel = $db !== null ? $db['label'] : 'Database'; + $adminCmd = ($db !== null && $db['product'] === 'mariadb') ? 'mariadb-admin' : 'mysqladmin'; + $dbChecked = false; + + try { + run("$adminCmd ping --silent 2>/dev/null"); + addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel responding"); + $dbChecked = true; + } catch (RunException) { + // Admin tool not available — fall through to process check + } + + if (!$dbChecked) { + $dbProcess = isServiceActive('mysqld', 'mariadbd'); + + if ($dbProcess !== null) { + addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel process running ($dbProcess)"); + } else { + addRequirementRow('Database server', REQUIREMENT_FAIL, 'No mysqld or mariadbd process found'); + } + } + + // 4. HTTP response + $url = get('requirements_health_url'); + + try { + $httpCode = (int) trim(run( + sprintf("curl -s -o /dev/null -w '%%{http_code}' --max-time 5 %s 2>/dev/null", escapeshellarg($url)) + )); + + if ($httpCode >= 200 && $httpCode < 500) { + addRequirementRow('HTTP response', REQUIREMENT_OK, "HTTP $httpCode from $url"); + } elseif ($httpCode === 0) { + addRequirementRow('HTTP response', REQUIREMENT_FAIL, "No response from $url (connection refused or timeout)"); + } else { + addRequirementRow('HTTP response', REQUIREMENT_FAIL, "HTTP $httpCode from $url"); + } + } catch (RunException) { + addRequirementRow('HTTP response', REQUIREMENT_SKIP, 'curl not available'); + } + + renderRequirementsTable(); +})->desc('Check service health'); diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index bcd34d7..3b40e49 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -107,6 +107,25 @@ Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflif The warning threshold is configurable (default: 6 months before EOL). +## Health check + +A standalone task that verifies critical services are running on the target host. This is useful as a quick smoke test before or after deployment. + +```bash +$ dep requirements:health [host] +``` + +The task checks four categories: + +| Check | Method | OK | FAIL | +|-------|--------|----|------| +| PHP-FPM | systemctl / pgrep for `php-fpm` | Process active | Process not found | +| Webserver | systemctl / pgrep for nginx, apache2, httpd | Process active | No process found | +| Database server | `mysqladmin ping` / `mariadb-admin ping` with process fallback | Responding or process running | No process found | +| HTTP response | `curl` against configured URL | HTTP 2xx–4xx | HTTP 5xx, timeout, or connection refused | + +Service detection uses `systemctl is-active` with a `pgrep` fallback for systems without systemd. + ## Configuration All settings use the `requirements_` prefix and can be overridden in the consuming project: @@ -162,6 +181,10 @@ set('requirements_eol_api_timeout', 5); // API timeout in seconds // Database grants check set('requirements_check_database_grants_enabled', true); + +// Health check +set('requirements_check_health_enabled', true); +set('requirements_health_url', 'https://example.com'); ``` ## Extending with custom checks