From f407e3d8b3e9d5f51372c4f8860f627517507bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Tue, 11 Nov 2025 11:49:56 +0200 Subject: [PATCH 1/3] feat: add dedicated install-php playbook and command --- app/Console/Server/ServerInstallCommand.php | 18 + .../Server/ServerInstallPhpCommand.php | 97 ++++ app/SymfonyApp.php | 2 + app/Traits/ServersTrait.php | 260 ++++++++--- playbooks/demo-site.sh | 38 +- playbooks/server-info.sh | 141 ++++-- playbooks/server-install-php.sh | 418 ++++++++++++++++++ playbooks/server-install.sh | 138 +----- 8 files changed, 892 insertions(+), 220 deletions(-) create mode 100644 app/Console/Server/ServerInstallPhpCommand.php create mode 100644 playbooks/server-install-php.sh diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index bf35c606..8fd63b0c 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -33,6 +33,8 @@ protected function configure(): void parent::configure(); $this->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name'); + $this->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to install'); + $this->addOption('php-default', null, InputOption::VALUE_NONE, 'Set as default PHP version'); } // ---- @@ -99,6 +101,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server installed successfully'); + // + // Install PHP + // ---- + + $phpResult = $this->installPhp($server, $info); + + if (is_int($phpResult)) { + return $phpResult; + } + + /** @var array{status: int, php_version: string, php_default: bool} $phpResult */ + $phpVersion = $phpResult['php_version']; + $phpDefault = $phpResult['php_default']; + // // Setup demo site // ---- @@ -152,6 +168,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->showCommandReplay('server:install', [ 'server' => $server->name, + 'php-version' => $phpVersion, + 'php-default' => $phpDefault, ]); return Command::SUCCESS; diff --git a/app/Console/Server/ServerInstallPhpCommand.php b/app/Console/Server/ServerInstallPhpCommand.php new file mode 100644 index 00000000..dd75802b --- /dev/null +++ b/app/Console/Server/ServerInstallPhpCommand.php @@ -0,0 +1,97 @@ +addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name'); + $this->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to install'); + $this->addOption('php-default', null, InputOption::VALUE_NONE, 'Set as default PHP version'); + } + + // ---- + // Execution + // ---- + + protected function execute(InputInterface $input, OutputInterface $output): int + { + parent::execute($input, $output); + + $this->heading('Install PHP'); + + // + // Select server & display details + // ---- + + $server = $this->selectServer(); + + if (is_int($server)) { + return $server; + } + + $this->displayServerDeets($server); + + // + // Get server info (verifies SSH connection and validates distribution & permissions) + // ---- + + $info = $this->getServerInfo($server); + + if (is_int($info)) { + return $info; + } + + // + // Install PHP + // ---- + + $phpResult = $this->installPhp($server, $info); + + if (is_int($phpResult)) { + return $phpResult; + } + + /** @var array{status: int, php_version: string, php_default: bool} $phpResult */ + $phpVersion = $phpResult['php_version']; + $phpDefault = $phpResult['php_default']; + + // + // Show command replay + // ---- + + $this->showCommandReplay('server:install:php', [ + 'server' => $server->name, + 'php-version' => $phpVersion, + 'php-default' => $phpDefault, + ]); + + return Command::SUCCESS; + } + +} diff --git a/app/SymfonyApp.php b/app/SymfonyApp.php index b915c754..690ea658 100644 --- a/app/SymfonyApp.php +++ b/app/SymfonyApp.php @@ -12,6 +12,7 @@ use Bigpixelrocket\DeployerPHP\Console\Server\ServerDeleteCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerInfoCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerInstallCommand; +use Bigpixelrocket\DeployerPHP\Console\Server\ServerInstallPhpCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerListCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerLogsCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerProvisionDigitalOceanCommand; @@ -144,6 +145,7 @@ private function registerCommands(): void ServerListCommand::class, ServerInfoCommand::class, ServerInstallCommand::class, + ServerInstallPhpCommand::class, ServerLogsCommand::class, ServerRunCommand::class, diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 3e77346c..84810491 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -238,81 +238,121 @@ protected function displayServerInfo(array $info): void } } - // Display PHP-FPM information if available - if (isset($info['php_fpm']) && is_array($info['php_fpm']) && ($info['php_fpm']['available'] ?? false) === true) { - $phpFpmItems = []; - - if (isset($info['php_fpm']['pool']) && $info['php_fpm']['pool'] !== 'unknown') { - /** @var string $pool */ - $pool = $info['php_fpm']['pool']; - $phpFpmItems[] = 'Pool: '.$pool; + // Display PHP versions if available + if (isset($info['php']) && is_array($info['php'])) { + $phpItems = []; + + if (isset($info['php']['versions']) && is_array($info['php']['versions']) && count($info['php']['versions']) > 0) { + $versions = $info['php']['versions']; + $defaultVersion = $info['php']['default'] ?? null; + + foreach ($versions as $version) { + if (is_string($version) || is_numeric($version)) { + $versionStr = (string) $version; + if ($defaultVersion !== null && (is_string($defaultVersion) || is_numeric($defaultVersion))) { + /** @var string|int|float $defaultVersion */ + $isDefault = $versionStr === (string) $defaultVersion; + if ($isDefault) { + $phpItems[] = "PHP {$versionStr} (default)"; + } else { + $phpItems[] = "PHP {$versionStr}"; + } + } else { + $phpItems[] = "PHP {$versionStr}"; + } + } + } } - if (isset($info['php_fpm']['process_manager']) && $info['php_fpm']['process_manager'] !== 'unknown') { - /** @var string $processManager */ - $processManager = $info['php_fpm']['process_manager']; - $phpFpmItems[] = 'Process Manager: '.$processManager; + if (count($phpItems) === 0) { + $phpItems[] = 'No PHP installed'; } - if (isset($info['php_fpm']['active_processes'])) { - /** @var int|string $activeProcesses */ - $activeProcesses = $info['php_fpm']['active_processes']; - $phpFpmItems[] = 'Active: '.$activeProcesses.' processes'; - } + $this->io->displayDeets(['PHP' => $phpItems]); + $this->io->writeln(''); + } - if (isset($info['php_fpm']['idle_processes'])) { - /** @var int|string $idleProcesses */ - $idleProcesses = $info['php_fpm']['idle_processes']; - $phpFpmItems[] = 'Idle: '.$idleProcesses.' processes'; - } + // Display PHP-FPM information if available (multiple versions) + if (isset($info['php_fpm']) && is_array($info['php_fpm']) && count($info['php_fpm']) > 0) { + foreach ($info['php_fpm'] as $version => $fpmData) { + if (!is_array($fpmData) || !is_string($version)) { + continue; + } - if (isset($info['php_fpm']['total_processes'])) { - /** @var int|string $totalProcesses */ - $totalProcesses = $info['php_fpm']['total_processes']; - $phpFpmItems[] = 'Total: '.$totalProcesses.' processes'; - } + $phpFpmItems = []; - if (isset($info['php_fpm']['listen_queue'])) { - /** @var int|string|float $rawQueue */ - $rawQueue = $info['php_fpm']['listen_queue']; - /** @var int $queue */ - $queue = (int) $rawQueue; - $queueDisplay = $queue > 0 ? "{$queue} waiting" : '0 waiting'; - $phpFpmItems[] = 'Queue: '.$queueDisplay; - } + if (isset($fpmData['pool']) && $fpmData['pool'] !== 'unknown') { + /** @var string $pool */ + $pool = $fpmData['pool']; + $phpFpmItems[] = 'Pool: '.$pool; + } - if (isset($info['php_fpm']['accepted_conn'])) { - /** @var int|string|float $rawAccepted */ - $rawAccepted = $info['php_fpm']['accepted_conn']; - /** @var int $accepted */ - $accepted = (int) $rawAccepted; - $phpFpmItems[] = 'Accepted: '.number_format($accepted); - } + if (isset($fpmData['process_manager']) && $fpmData['process_manager'] !== 'unknown') { + /** @var string $processManager */ + $processManager = $fpmData['process_manager']; + $phpFpmItems[] = 'Process Manager: '.$processManager; + } - if (isset($info['php_fpm']['max_children_reached'])) { - /** @var int|string|float $rawMaxChildren */ - $rawMaxChildren = $info['php_fpm']['max_children_reached']; - /** @var int $maxChildren */ - $maxChildren = (int) $rawMaxChildren; - if ($maxChildren > 0) { - $phpFpmItems[] = "Max Children Reached: {$maxChildren}"; + if (isset($fpmData['active_processes'])) { + /** @var int|string $activeProcesses */ + $activeProcesses = $fpmData['active_processes']; + $phpFpmItems[] = 'Active: '.$activeProcesses.' processes'; } - } - if (isset($info['php_fpm']['slow_requests'])) { - /** @var int|string|float $rawSlowReqs */ - $rawSlowReqs = $info['php_fpm']['slow_requests']; - /** @var int $slowReqsInt */ - $slowReqsInt = (int) $rawSlowReqs; - if ($slowReqsInt > 0) { - $slowReqs = number_format($slowReqsInt); - $phpFpmItems[] = "Slow Requests: {$slowReqs}"; + if (isset($fpmData['idle_processes'])) { + /** @var int|string $idleProcesses */ + $idleProcesses = $fpmData['idle_processes']; + $phpFpmItems[] = 'Idle: '.$idleProcesses.' processes'; } - } - if (count($phpFpmItems) > 0) { - $this->io->displayDeets(['PHP-FPM' => $phpFpmItems]); - $this->io->writeln(''); + if (isset($fpmData['total_processes'])) { + /** @var int|string $totalProcesses */ + $totalProcesses = $fpmData['total_processes']; + $phpFpmItems[] = 'Total: '.$totalProcesses.' processes'; + } + + if (isset($fpmData['listen_queue'])) { + /** @var int|string|float $rawQueue */ + $rawQueue = $fpmData['listen_queue']; + /** @var int $queue */ + $queue = (int) $rawQueue; + $queueDisplay = $queue > 0 ? "{$queue} waiting" : '0 waiting'; + $phpFpmItems[] = 'Queue: '.$queueDisplay; + } + + if (isset($fpmData['accepted_conn'])) { + /** @var int|string|float $rawAccepted */ + $rawAccepted = $fpmData['accepted_conn']; + /** @var int $accepted */ + $accepted = (int) $rawAccepted; + $phpFpmItems[] = 'Accepted: '.number_format($accepted); + } + + if (isset($fpmData['max_children_reached'])) { + /** @var int|string|float $rawMaxChildren */ + $rawMaxChildren = $fpmData['max_children_reached']; + /** @var int $maxChildren */ + $maxChildren = (int) $rawMaxChildren; + if ($maxChildren > 0) { + $phpFpmItems[] = "Max Children Reached: {$maxChildren}"; + } + } + + if (isset($fpmData['slow_requests'])) { + /** @var int|string|float $rawSlowReqs */ + $rawSlowReqs = $fpmData['slow_requests']; + /** @var int $slowReqsInt */ + $slowReqsInt = (int) $rawSlowReqs; + if ($slowReqsInt > 0) { + $slowReqs = number_format($slowReqsInt); + $phpFpmItems[] = "Slow Requests: {$slowReqs}"; + } + } + + if (count($phpFpmItems) > 0) { + $this->io->displayDeets(["PHP-FPM {$version}" => $phpFpmItems]); + $this->io->writeln(''); + } } } } @@ -345,6 +385,102 @@ private function formatUptime(int $seconds): string return "{$days}d {$hours}h"; } + /** + * Install PHP on a server. + * + * Prompts for PHP version selection and handles installation via playbook. + * Automatically sets first PHP install as default, otherwise prompts user. + * + * @param ServerDTO $server Server to install PHP on + * @param array $info Server information from getServerInfo() + * @return array{status: int, php_version: string, php_default: bool}|int Returns array with status and values, or int on failure + */ + protected function installPhp(ServerDTO $server, array $info): array|int + { + $phpVersions = ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; + + // + // Extract installed PHP versions + // ---- + + $installedPhpVersions = []; + if (isset($info['php']) && is_array($info['php']) && isset($info['php']['versions']) && is_array($info['php']['versions'])) { + foreach ($info['php']['versions'] as $version) { + if (is_string($version) || is_numeric($version)) { + $installedPhpVersions[] = (string) $version; + } + } + } + + // + // Prompt for version to install + // ---- + + $phpVersion = (string) $this->io->getOptionOrPrompt( + 'php-version', + fn () => $this->io->promptSelect( + label: 'PHP version:', + options: $phpVersions, + default: '8.4' + ) + ); + + // + // Determine if setting as default + // ---- + + if (count($installedPhpVersions) === 0) { + // First PHP install - automatically set as default + $setAsDefault = true; + } else { + // PHP already installed - ask user + $setAsDefault = (bool) $this->io->getOptionOrPrompt( + 'php-default', + fn () => $this->io->promptConfirm( + label: "Set PHP {$phpVersion} as default?", + default: false + ) + ); + } + + // + // Execute installation playbook + // ---- + + /** @var string $distro */ + $distro = $info['distro']; + /** @var string $permissions */ + $permissions = $info['permissions']; + + $result = $this->executePlaybook( + $server, + 'server-install-php', + "Installing PHP {$phpVersion}...", + [ + 'DEPLOYER_DISTRO' => $distro, + 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_PHP_VERSION' => $phpVersion, + 'DEPLOYER_PHP_SET_DEFAULT' => $setAsDefault ? 'true' : 'false', + ], + true + ); + + if (is_int($result)) { + $this->io->error('PHP installation failed'); + + return Command::FAILURE; + } + + $defaultStatus = $setAsDefault ? ' (set as default)' : ''; + $this->yay("PHP {$phpVersion} installed successfully{$defaultStatus}"); + + return [ + 'status' => Command::SUCCESS, + 'php_version' => $phpVersion, + 'php_default' => $setAsDefault, + ]; + } + // // UI // ---- diff --git a/playbooks/demo-site.sh b/playbooks/demo-site.sh index 0852d37e..0c74269c 100644 --- a/playbooks/demo-site.sh +++ b/playbooks/demo-site.sh @@ -42,6 +42,31 @@ run_cmd() { fi } +# +# Detect default PHP version + +detect_php_default() { + local default_version + + # Try update-alternatives first + if command -v update-alternatives > /dev/null 2>&1; then + default_version=$(update-alternatives --query php 2> /dev/null | grep '^Value:' | awk '{print $2}') + if [[ -n $default_version && $default_version =~ php([0-9]+\.[0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return + fi + fi + + # Fallback: check /usr/bin/php directly + if [[ -x /usr/bin/php ]]; then + default_version=$(/usr/bin/php -v 2> /dev/null | head -n1 | grep -oP 'PHP \K[0-9]+\.[0-9]+') + if [[ -n $default_version ]]; then + echo "$default_version" + return + fi + fi +} + # ---- # Setup Functions # ---- @@ -132,8 +157,19 @@ setup_demo_site() { configure_demo_site() { echo "✓ Configuring demo site..." + # Detect default PHP version + local php_version + php_version=$(detect_php_default) + + if [[ -z $php_version ]]; then + echo "Error: No default PHP version found. Run server:install to install PHP first." >&2 + exit 1 + fi + + echo "✓ Using PHP ${php_version} (default)" + # PHP-FPM socket path (debian family) - local php_fpm_socket='/run/php/php8.4-fpm.sock' + local php_fpm_socket="/run/php/php${php_version}-fpm.sock" # Create log directory if [[ ! -d /var/log/caddy ]]; then diff --git a/playbooks/server-info.sh b/playbooks/server-info.sh index d7ee95a4..d875bad9 100755 --- a/playbooks/server-info.sh +++ b/playbooks/server-info.sh @@ -12,8 +12,9 @@ # - family: debian|fedora|redhat|amazon|unknown # - permissions: root|sudo|none # - hardware: cpu_cores, ram_mb, disk_type +# - php: versions array, default version # - caddy: Caddy metrics (available, version, sites_count, domains, uptime_seconds, active_requests, total_requests, memory_mb) -# - php_fpm: PHP-FPM metrics (available, pool, process_manager, uptime_seconds, accepted_conn, listen_queue, idle_processes, active_processes, total_processes, max_children_reached, slow_requests) +# - php_fpm: map of PHP versions to metrics (pool, process_manager, uptime_seconds, accepted_conn, listen_queue, idle_processes, active_processes, total_processes, max_children_reached, slow_requests) # - ports: map of port numbers to process names set -o pipefail @@ -202,6 +203,58 @@ detect_disk_type() { fi } +# +# PHP Detection +# ---- + +# +# Detect installed PHP versions + +detect_php_versions() { + local version_list=() + + # Find all php binaries in /usr/bin + while IFS= read -r binary; do + # Extract version from binary name (e.g., php8.4 -> 8.4) + if [[ $binary =~ php([0-9]+\.[0-9]+)$ ]]; then + version_list+=("${BASH_REMATCH[1]}") + fi + done < <(find /usr/bin -maxdepth 1 -name 'php[0-9]*' -type f 2> /dev/null | sort -V) + + # Return comma-separated list + if ((${#version_list[@]} > 0)); then + printf '%s' "$( + IFS=, + echo "${version_list[*]}" + )" + fi +} + +# +# Detect default PHP version + +detect_php_default() { + local default_version + + # Try update-alternatives first + if command -v update-alternatives > /dev/null 2>&1; then + default_version=$(update-alternatives --query php 2> /dev/null | grep '^Value:' | awk '{print $2}') + if [[ -n $default_version && $default_version =~ php([0-9]+\.[0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return + fi + fi + + # Fallback: check /usr/bin/php directly + if [[ -x /usr/bin/php ]]; then + default_version=$(/usr/bin/php -v 2> /dev/null | head -n1 | grep -oP 'PHP \K[0-9]+\.[0-9]+') + if [[ -n $default_version ]]; then + echo "$default_version" + return + fi + fi +} + # ---- # Service Metrics # ---- @@ -318,17 +371,19 @@ get_caddy_metrics() { # ---- # -# Query PHP-FPM status page and extract metrics +# Query PHP-FPM status page for a specific PHP version get_php_fpm_metrics() { - # Check if PHP-FPM status endpoint is available on localhost:9001 - if ! curl -sf --max-time 2 http://localhost:9001/fpm-status > /dev/null 2>&1; then + local php_version=$1 + + # Check if PHP-FPM status endpoint is available for this version + if ! curl -sf --max-time 2 "http://localhost:9001/php${php_version}/fpm-status" > /dev/null 2>&1; then return 0 fi # Get status in JSON format local status - status=$(curl -sf --max-time 2 'http://localhost:9001/fpm-status?json' 2> /dev/null || echo "{}") + status=$(curl -sf --max-time 2 "http://localhost:9001/php${php_version}/fpm-status?json" 2> /dev/null || echo "{}") # Parse JSON fields using grep/sed (simple parsing without jq dependency) local pool process_manager start_since accepted_conn @@ -376,6 +431,7 @@ get_php_fpm_metrics() { main() { local distro family permissions local cpu_cores ram_mb disk_type + local php_versions php_default # # Gather basic info @@ -395,6 +451,10 @@ main() { ram_mb=$(detect_ram_mb) disk_type=$(detect_disk_type) + echo "✓ Detecting PHP versions..." + php_versions=$(detect_php_versions) + php_default=$(detect_php_default) + echo "✓ Checking Caddy status..." local caddy_metrics caddy_available="false" local caddy_version caddy_sites caddy_domains caddy_uptime caddy_active_req caddy_total_req caddy_memory @@ -406,15 +466,6 @@ main() { fi echo "✓ Checking PHP-FPM status..." - local php_fpm_metrics php_fpm_available="false" - local php_fpm_pool php_fpm_pm php_fpm_uptime php_fpm_accepted php_fpm_queue - local php_fpm_idle php_fpm_active php_fpm_total php_fpm_max_children php_fpm_slow - php_fpm_metrics=$(get_php_fpm_metrics) - - if [[ -n $php_fpm_metrics ]]; then - php_fpm_available="true" - IFS=$'\t' read -r php_fpm_pool php_fpm_pm php_fpm_uptime php_fpm_accepted php_fpm_queue php_fpm_idle php_fpm_active php_fpm_total php_fpm_max_children php_fpm_slow <<< "$php_fpm_metrics" - fi # # Output YAML to file @@ -427,6 +478,9 @@ main() { cpu_cores: $cpu_cores ram_mb: $ram_mb disk_type: $disk_type + php: + versions: [${php_versions}] + default: ${php_default:-} caddy: available: $caddy_available version: ${caddy_version:-unknown} @@ -437,23 +491,58 @@ main() { total_requests: ${caddy_total_req:-0} memory_mb: ${caddy_memory:-0} php_fpm: - available: $php_fpm_available - pool: ${php_fpm_pool:-www} - process_manager: ${php_fpm_pm:-unknown} - uptime_seconds: ${php_fpm_uptime:-0} - accepted_conn: ${php_fpm_accepted:-0} - listen_queue: ${php_fpm_queue:-0} - idle_processes: ${php_fpm_idle:-0} - active_processes: ${php_fpm_active:-0} - total_processes: ${php_fpm_total:-0} - max_children_reached: ${php_fpm_max_children:-0} - slow_requests: ${php_fpm_slow:-0} - ports: EOF echo "Error: Failed to write $DEPLOYER_OUTPUT_FILE" >&2 exit 1 fi + # Add PHP-FPM metrics for each installed version + local has_fpm_metrics=false + if [[ -n $php_versions ]]; then + IFS=',' read -ra version_array <<< "$php_versions" + for version in "${version_array[@]}"; do + local fpm_metrics + fpm_metrics=$(get_php_fpm_metrics "$version") + + if [[ -n $fpm_metrics ]]; then + has_fpm_metrics=true + local pool pm uptime accepted queue idle active total max_children slow + IFS=$'\t' read -r pool pm uptime accepted queue idle active total max_children slow <<< "$fpm_metrics" + + if ! cat >> "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + "${version}": + pool: ${pool} + process_manager: ${pm} + uptime_seconds: ${uptime} + accepted_conn: ${accepted} + listen_queue: ${queue} + idle_processes: ${idle} + active_processes: ${active} + total_processes: ${total} + max_children_reached: ${max_children} + slow_requests: ${slow} + EOF + echo "Error: Failed to write PHP-FPM metrics to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + fi + done + fi + + # If no PHP-FPM metrics were found, write empty object + if [[ $has_fpm_metrics == false ]]; then + if ! echo " {}" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write empty PHP-FPM section to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + fi + + # Add ports section + if ! echo "ports:" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write ports section to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + local port process has_ports=false while IFS=: read -r port process; do if ! echo " ${port}: ${process}" >> "$DEPLOYER_OUTPUT_FILE"; then diff --git a/playbooks/server-install-php.sh b/playbooks/server-install-php.sh new file mode 100644 index 00000000..66dea6da --- /dev/null +++ b/playbooks/server-install-php.sh @@ -0,0 +1,418 @@ +#!/usr/bin/env bash + +# +# PHP Installation Playbook - Ubuntu/Debian Only +# +# Install specified PHP version with FPM and common extensions +# ---- +# +# This playbook only supports Ubuntu and Debian distributions (debian family). +# Both distributions use apt package manager and follow debian conventions. +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian +# DEPLOYER_PERMS - Permissions: root|sudo +# DEPLOYER_PHP_VERSION - PHP version to install (e.g., 8.4, 8.3, 7.4) +# DEPLOYER_PHP_SET_DEFAULT - Set as system default: true|false +# +# Returns YAML with: +# - status: success +# - php_version: installed PHP version +# - is_default: whether this version is set as system default +# - fpm_socket_path: path to PHP-FPM socket +# - tasks_completed: list of completed tasks +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +[[ -z $DEPLOYER_PHP_VERSION ]] && echo "Error: DEPLOYER_PHP_VERSION required" && exit 1 +[[ -z $DEPLOYER_PHP_SET_DEFAULT ]] && echo "Error: DEPLOYER_PHP_SET_DEFAULT required" && exit 1 +export DEPLOYER_PERMS + +# ---- +# Helpers +# ---- + +# +# Permission Management +# ---- + +# +# Execute command with appropriate permissions + +run_cmd() { + if [[ $DEPLOYER_PERMS == 'root' ]]; then + "$@" + else + sudo -n "$@" + fi +} + +# +# Package Management +# ---- + +# +# Wait for dpkg lock to be released + +wait_for_dpkg_lock() { + local max_wait=60 + local waited=0 + local lock_found=false + + # Check multiple times to catch the lock even in race conditions + while ((waited < max_wait)); do + # Try to acquire the lock by checking if we can open it + if fuser /var/lib/dpkg/lock-frontend > /dev/null 2>&1 \ + || fuser /var/lib/dpkg/lock > /dev/null 2>&1 \ + || fuser /var/lib/apt/lists/lock > /dev/null 2>&1; then + lock_found=true + echo "✓ Waiting for package manager lock to be released..." + sleep 2 + waited=$((waited + 2)) + else + # Lock not held, but wait a bit to ensure it's really released + if [[ $lock_found == true ]]; then + # Was locked before, give it extra time + sleep 2 + else + # Never saw lock, just a small delay + sleep 1 + fi + return 0 + fi + done + + echo "Error: Timeout waiting for dpkg lock to be released" >&2 + return 1 +} + +# +# apt-get with retry + +apt_get_with_retry() { + local max_attempts=5 + local attempt=1 + local wait_time=10 + local output + + while ((attempt <= max_attempts)); do + # Capture output to check for lock errors + output=$(run_cmd apt-get "$@" 2>&1) + local exit_code=$? + + if ((exit_code == 0)); then + [[ -n $output ]] && echo "$output" + return 0 + fi + + # Only retry on lock-related errors + if echo "$output" | grep -qE 'Could not get lock|dpkg.*lock|Unable to acquire'; then + if ((attempt < max_attempts)); then + echo "✓ Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." + sleep "$wait_time" + wait_time=$((wait_time + 5)) + attempt=$((attempt + 1)) + wait_for_dpkg_lock || true + else + echo "$output" >&2 + return "$exit_code" + fi + else + # Non-lock error, fail immediately + echo "$output" >&2 + return "$exit_code" + fi + done + + return 1 +} + +# ---- +# Installation Functions +# ---- + +# +# Repository Setup +# ---- + +# +# Setup PHP repository + +setup_php_repository() { + echo "✓ Setting up PHP repository..." + + case $DEPLOYER_DISTRO in + ubuntu) + # PHP PPA (Ubuntu only) + if ! grep -qr "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2> /dev/null; then + if ! run_cmd env DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:ondrej/php 2>&1; then + echo "Error: Failed to add PHP PPA" >&2 + exit 1 + fi + fi + ;; + debian) + # Sury PHP repository (Debian only) + if ! [[ -f /usr/share/keyrings/php-sury-archive-keyring.gpg ]]; then + if ! curl -fsSL 'https://packages.sury.org/php/apt.gpg' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/php-sury-archive-keyring.gpg; then + echo "Error: Failed to add Sury PHP GPG key" >&2 + exit 1 + fi + fi + + if ! [[ -f /etc/apt/sources.list.d/php-sury.list ]]; then + local debian_codename + debian_codename=$(lsb_release -sc) + if ! echo "deb [signed-by=/usr/share/keyrings/php-sury-archive-keyring.gpg] https://packages.sury.org/php/ ${debian_codename} main" | run_cmd tee /etc/apt/sources.list.d/php-sury.list > /dev/null; then + echo "Error: Failed to add Sury PHP repository" >&2 + exit 1 + fi + fi + ;; + esac +} + +# +# Package Installation +# ---- + +# +# Install PHP packages for specified version + +install_php_packages() { + echo "✓ Installing PHP ${DEPLOYER_PHP_VERSION}..." + + # Update package lists + echo "✓ Updating package lists..." + if ! apt_get_with_retry update -q; then + echo "Error: Failed to update package lists" >&2 + exit 1 + fi + + # Install PHP packages + if ! apt_get_with_retry install -y -q --no-install-recommends \ + php${DEPLOYER_PHP_VERSION}-cli \ + php${DEPLOYER_PHP_VERSION}-fpm \ + php${DEPLOYER_PHP_VERSION}-common \ + php${DEPLOYER_PHP_VERSION}-opcache \ + php${DEPLOYER_PHP_VERSION}-bcmath \ + php${DEPLOYER_PHP_VERSION}-curl \ + php${DEPLOYER_PHP_VERSION}-mbstring \ + php${DEPLOYER_PHP_VERSION}-xml \ + php${DEPLOYER_PHP_VERSION}-zip \ + php${DEPLOYER_PHP_VERSION}-gd \ + php${DEPLOYER_PHP_VERSION}-intl \ + php${DEPLOYER_PHP_VERSION}-soap 2>&1; then + echo "Error: Failed to install PHP ${DEPLOYER_PHP_VERSION} packages" >&2 + exit 1 + fi +} + +# +# PHP-FPM Configuration +# ---- + +# +# Configure PHP-FPM for the installed version + +configure_php_fpm() { + echo "✓ Configuring PHP-FPM..." + + local pool_config="/etc/php/${DEPLOYER_PHP_VERSION}/fpm/pool.d/www.conf" + + # Set socket ownership so Caddy can access it + if ! run_cmd sed -i 's/^;listen.owner = .*/listen.owner = caddy/' "$pool_config"; then + echo "Error: Failed to set PHP-FPM socket owner" >&2 + exit 1 + fi + if ! run_cmd sed -i 's/^;listen.group = .*/listen.group = caddy/' "$pool_config"; then + echo "Error: Failed to set PHP-FPM socket group" >&2 + exit 1 + fi + if ! run_cmd sed -i 's/^;listen.mode = .*/listen.mode = 0660/' "$pool_config"; then + echo "Error: Failed to set PHP-FPM socket mode" >&2 + exit 1 + fi + + # Enable PHP-FPM status page + if ! run_cmd sed -i 's/^;pm.status_path = .*/pm.status_path = \/fpm-status/' "$pool_config"; then + echo "Error: Failed to enable PHP-FPM status page" >&2 + exit 1 + fi + + # Enable and start PHP-FPM service + if ! systemctl is-enabled --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then + if ! run_cmd systemctl enable --quiet php${DEPLOYER_PHP_VERSION}-fpm; then + echo "Error: Failed to enable PHP-FPM service" >&2 + exit 1 + fi + fi + if ! systemctl is-active --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then + if ! run_cmd systemctl start php${DEPLOYER_PHP_VERSION}-fpm; then + echo "Error: Failed to start PHP-FPM service" >&2 + exit 1 + fi + fi +} + +# +# Default Version Configuration +# ---- + +# +# Set PHP version as system default + +set_as_default() { + if [[ $DEPLOYER_PHP_SET_DEFAULT != 'true' ]]; then + return 0 + fi + + echo "✓ Setting PHP ${DEPLOYER_PHP_VERSION} as system default..." + + # Set alternatives for php binaries + if command -v update-alternatives > /dev/null 2>&1; then + if run_cmd update-alternatives --set php /usr/bin/php${DEPLOYER_PHP_VERSION} 2> /dev/null; then + echo "✓ Set php alternative" + fi + + if run_cmd update-alternatives --set php-config /usr/bin/php-config${DEPLOYER_PHP_VERSION} 2> /dev/null; then + echo "✓ Set php-config alternative" + fi + + if run_cmd update-alternatives --set phpize /usr/bin/phpize${DEPLOYER_PHP_VERSION} 2> /dev/null; then + echo "✓ Set phpize alternative" + fi + fi +} + +# +# Caddy Configuration +# ---- + +# +# Update Caddy localhost configuration with PHP-FPM endpoint + +update_caddy_config() { + if ! [[ -f /etc/caddy/conf.d/localhost.caddy ]]; then + echo "Warning: localhost.caddy not found, skipping Caddy configuration" + return 0 + fi + + echo "✓ Updating Caddy localhost configuration..." + + # Check if the base server block exists + if ! grep -q "http://localhost:9001" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then + # Create base server block structure + if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then + # PHP-FPM status endpoints - localhost only (not accessible from internet) + http://localhost:9001 { + } + EOF + echo "Error: Failed to create Caddy localhost configuration" >&2 + exit 1 + fi + fi + + # Check if this PHP version's endpoint already exists + if grep -q "handle_path /php${DEPLOYER_PHP_VERSION}/" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then + echo "✓ PHP ${DEPLOYER_PHP_VERSION} endpoint already configured" + return 0 + fi + + # Create temporary file with the new handle block + local temp_handle + temp_handle=$(mktemp) + + cat > "$temp_handle" <<- EOF + handle_path /php${DEPLOYER_PHP_VERSION}/* { + reverse_proxy unix//run/php/php${DEPLOYER_PHP_VERSION}-fpm.sock { + transport fastcgi { + env SCRIPT_FILENAME /fpm-status + env SCRIPT_NAME /fpm-status + } + } + } + EOF + + # Insert the handle block before the closing brace of the server block + local temp_config + temp_config=$(mktemp) + + # Read the file, insert handle block before last closing brace + if ! awk -v handle="$(cat "$temp_handle")" ' + /^}$/ && !found { + print handle + found=1 + } + { print } + ' /etc/caddy/conf.d/localhost.caddy > "$temp_config"; then + rm -f "$temp_handle" "$temp_config" + echo "Error: Failed to update Caddy configuration" >&2 + exit 1 + fi + + # Replace the original file + if ! run_cmd cp "$temp_config" /etc/caddy/conf.d/localhost.caddy; then + rm -f "$temp_handle" "$temp_config" + echo "Error: Failed to write Caddy configuration" >&2 + exit 1 + fi + + rm -f "$temp_handle" "$temp_config" + + # Reload Caddy to apply changes + if systemctl is-active --quiet caddy 2> /dev/null; then + if ! run_cmd systemctl reload caddy 2> /dev/null; then + echo "Warning: Failed to reload Caddy configuration" + fi + fi +} + +# ---- +# Main Execution +# ---- + +main() { + local fpm_socket_path="/run/php/php${DEPLOYER_PHP_VERSION}-fpm.sock" + local is_default="false" + + # Execute installation tasks + setup_php_repository + install_php_packages + configure_php_fpm + set_as_default + update_caddy_config + + if [[ $DEPLOYER_PHP_SET_DEFAULT == 'true' ]]; then + is_default="true" + fi + + # Get actual PHP version + local php_version + php_version=$(php${DEPLOYER_PHP_VERSION} -r "echo PHP_VERSION;" 2> /dev/null || echo "$DEPLOYER_PHP_VERSION") + + # Write output YAML + if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + status: success + php_version: $php_version + is_default: $is_default + fpm_socket_path: $fpm_socket_path + tasks_completed: + - setup_php_repository + - install_php_packages + - configure_php_fpm + - set_as_default + - update_caddy_config + EOF + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index a743a6f3..b55fc37c 100644 --- a/playbooks/server-install.sh +++ b/playbooks/server-install.sh @@ -3,12 +3,14 @@ # # Server Installation Playbook - Ubuntu/Debian Only # -# Install Caddy, PHP 8.4, PHP-FPM, Git, Bun +# Install Caddy, Git, Bun, and setup deploy user # ---- # # This playbook only supports Ubuntu and Debian distributions (debian family). # Both distributions use apt package manager and follow debian conventions. # +# Note: PHP installation is handled by a separate playbook (server-install-php.sh) +# # Required Environment Variables: # DEPLOYER_OUTPUT_FILE - Output file path # DEPLOYER_DISTRO - Exact distribution: ubuntu|debian @@ -18,7 +20,6 @@ # Returns YAML with: # - status: success # - distro: detected distribution -# - php_version: installed PHP version # - caddy_version: installed Caddy version # - git_version: installed Git version # - bun_version: installed Bun version @@ -162,37 +163,6 @@ setup_repositories() { exit 1 fi fi - - # PHP repository (distribution-specific) - case $DEPLOYER_DISTRO in - ubuntu) - # PHP PPA (Ubuntu only) - if ! grep -qr "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2> /dev/null; then - if ! run_cmd env DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:ondrej/php 2>&1; then - echo "Error: Failed to add PHP PPA" >&2 - exit 1 - fi - fi - ;; - debian) - # Sury PHP repository (Debian only) - if ! [[ -f /usr/share/keyrings/php-sury-archive-keyring.gpg ]]; then - if ! curl -fsSL 'https://packages.sury.org/php/apt.gpg' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/php-sury-archive-keyring.gpg; then - echo "Error: Failed to add Sury PHP GPG key" >&2 - exit 1 - fi - fi - - if ! [[ -f /etc/apt/sources.list.d/php-sury.list ]]; then - local debian_codename - debian_codename=$(lsb_release -sc) - if ! echo "deb [signed-by=/usr/share/keyrings/php-sury-archive-keyring.gpg] https://packages.sury.org/php/ ${debian_codename} main" | run_cmd tee /etc/apt/sources.list.d/php-sury.list > /dev/null; then - echo "Error: Failed to add Sury PHP repository" >&2 - exit 1 - fi - fi - ;; - esac } # @@ -252,61 +222,6 @@ install_all_packages() { echo "Error: Failed to install main packages" >&2 exit 1 fi - - # Install PHP 8.4 - echo "✓ Installing PHP 8.4..." - if ! apt_get_with_retry install -y -q --no-install-recommends \ - php8.4-cli \ - php8.4-fpm \ - php8.4-common \ - php8.4-opcache \ - php8.4-bcmath \ - php8.4-curl \ - php8.4-mbstring \ - php8.4-xml \ - php8.4-zip \ - php8.4-gd \ - php8.4-intl \ - php8.4-soap 2>&1; then - echo "Error: Failed to install PHP 8.4 packages" >&2 - exit 1 - fi - - # Configure PHP-FPM - echo "✓ Configuring PHP-FPM..." - - # Set socket ownership so Caddy can access it - if ! run_cmd sed -i 's/^;listen.owner = .*/listen.owner = caddy/' /etc/php/8.4/fpm/pool.d/www.conf; then - echo "Error: Failed to set PHP-FPM socket owner" >&2 - exit 1 - fi - if ! run_cmd sed -i 's/^;listen.group = .*/listen.group = caddy/' /etc/php/8.4/fpm/pool.d/www.conf; then - echo "Error: Failed to set PHP-FPM socket group" >&2 - exit 1 - fi - if ! run_cmd sed -i 's/^;listen.mode = .*/listen.mode = 0660/' /etc/php/8.4/fpm/pool.d/www.conf; then - echo "Error: Failed to set PHP-FPM socket mode" >&2 - exit 1 - fi - - # Enable PHP-FPM status page - if ! run_cmd sed -i 's/^;pm.status_path = .*/pm.status_path = \/fpm-status/' /etc/php/8.4/fpm/pool.d/www.conf; then - echo "Error: Failed to enable PHP-FPM status page" >&2 - exit 1 - fi - - if ! systemctl is-enabled --quiet php8.4-fpm 2> /dev/null; then - if ! run_cmd systemctl enable --quiet php8.4-fpm; then - echo "Error: Failed to enable PHP-FPM service" >&2 - exit 1 - fi - fi - if ! systemctl is-active --quiet php8.4-fpm 2> /dev/null; then - if ! run_cmd systemctl start php8.4-fpm; then - echo "Error: Failed to start PHP-FPM service" >&2 - exit 1 - fi - fi } # @@ -365,18 +280,10 @@ setup_caddy_structure() { fi # Create localhost.caddy - monitoring endpoints only accessible via localhost + # (PHP-FPM status endpoint will be added by PHP installation playbook) if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then - # PHP-FPM status endpoint - localhost only (not accessible from internet) - http://localhost:9001 { - handle { - reverse_proxy unix//run/php/php8.4-fpm.sock { - transport fastcgi { - env SCRIPT_FILENAME /fpm-status - env SCRIPT_NAME /fpm-status - } - } - } - } + # Localhost-only endpoints configuration + # PHP-FPM status endpoint will be configured during PHP installation EOF echo "Error: Failed to create localhost.caddy" >&2 exit 1 @@ -591,48 +498,21 @@ setup_deploy_directories() { # Validation # ---- -# -# Validate PHP version meets minimum requirements - -validate_php_version() { - local php_version - php_version=$(php -r "echo PHP_VERSION;" 2> /dev/null || echo "unknown") - - if [[ $php_version == "unknown" ]]; then - echo "Error: PHP installation failed or PHP not in PATH" >&2 - exit 1 - fi - - # Extract major.minor version - local php_major_minor - php_major_minor=$(echo "$php_version" | cut -d. -f1,2) - - # Check if below 8.3 using awk - if awk "BEGIN {exit !($php_major_minor < 8.3)}"; then - echo "Error: PHP $php_version is below minimum required version 8.3" >&2 - exit 1 - fi - - echo "✓ PHP $php_version installed (meets minimum 8.3)" -} - # ---- # Main Execution # ---- main() { - local php_version caddy_version bun_version git_version deploy_public_key + local caddy_version bun_version git_version deploy_public_key # Execute installation tasks install_all_packages install_bun setup_caddy_structure - validate_php_version setup_deploy_key setup_deploy_directories # Get versions and public key - php_version=$(php -r "echo PHP_VERSION;" 2> /dev/null || echo "unknown") caddy_version=$(caddy version 2> /dev/null | head -n1 | awk '{print $1}' || echo "unknown") git_version=$(git --version 2> /dev/null | awk '{print $3}' || echo "unknown") bun_version=$(bun --version 2> /dev/null || echo "unknown") @@ -642,7 +522,6 @@ main() { if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then status: success distro: $DEPLOYER_DISTRO - php_version: $php_version caddy_version: $caddy_version git_version: $git_version bun_version: $bun_version @@ -650,9 +529,6 @@ main() { tasks_completed: - install_caddy - setup_caddy_structure - - install_php - - install_extensions - - configure_php_fpm - install_git - install_rsync - install_bun From c8ec03c3d623cf6114b064bae93793bc724f1bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Tue, 11 Nov 2025 12:46:04 +0200 Subject: [PATCH 2/3] feat(playbooks): extract shared helpers and fix remote execution - Create playbooks/helpers.sh with 4 shared bash functions - Update all playbooks to comment out source lines with explanation - Modify executePlaybook() to automatically inline helpers.sh content - Fix 'helpers.sh: No such file or directory' error on remote servers - Reduce code duplication from ~200 lines to ~80 lines across playbooks --- app/Traits/PlaybooksTrait.php | 7 ++ playbooks/demo-site.sh | 41 +--------- playbooks/helpers.sh | 133 ++++++++++++++++++++++++++++++++ playbooks/server-info.sh | 43 +---------- playbooks/server-install-php.sh | 100 +----------------------- playbooks/server-install.sh | 100 +----------------------- 6 files changed, 149 insertions(+), 275 deletions(-) create mode 100644 playbooks/helpers.sh diff --git a/app/Traits/PlaybooksTrait.php b/app/Traits/PlaybooksTrait.php index ad20b163..a148c5aa 100644 --- a/app/Traits/PlaybooksTrait.php +++ b/app/Traits/PlaybooksTrait.php @@ -53,6 +53,13 @@ protected function executePlaybook( $playbookPath = $projectRoot . '/playbooks/' . $playbookName . '.sh'; $scriptContents = $this->fs->readFile($playbookPath); + // Prepend helpers.sh content to playbook for remote execution + $helpersPath = $projectRoot . '/playbooks/helpers.sh'; + if (file_exists($helpersPath)) { + $helpersContents = $this->fs->readFile($helpersPath); + $scriptContents = $helpersContents . "\n\n" . $scriptContents; + } + // Unique output file name $outputFile = sprintf('/tmp/deployer-output-%d-%s.yml', time(), bin2hex(random_bytes(8))); diff --git a/playbooks/demo-site.sh b/playbooks/demo-site.sh index 0c74269c..03cd6fdd 100644 --- a/playbooks/demo-site.sh +++ b/playbooks/demo-site.sh @@ -27,45 +27,8 @@ export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 export DEPLOYER_PERMS -# ---- -# Helpers -# ---- - -# -# Execute command with appropriate permissions - -run_cmd() { - if [[ $DEPLOYER_PERMS == 'root' ]]; then - "$@" - else - sudo -n "$@" - fi -} - -# -# Detect default PHP version - -detect_php_default() { - local default_version - - # Try update-alternatives first - if command -v update-alternatives > /dev/null 2>&1; then - default_version=$(update-alternatives --query php 2> /dev/null | grep '^Value:' | awk '{print $2}') - if [[ -n $default_version && $default_version =~ php([0-9]+\.[0-9]+)$ ]]; then - echo "${BASH_REMATCH[1]}" - return - fi - fi - - # Fallback: check /usr/bin/php directly - if [[ -x /usr/bin/php ]]; then - default_version=$(/usr/bin/php -v 2> /dev/null | head -n1 | grep -oP 'PHP \K[0-9]+\.[0-9]+') - if [[ -n $default_version ]]; then - echo "$default_version" - return - fi - fi -} +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" # ---- # Setup Functions diff --git a/playbooks/helpers.sh b/playbooks/helpers.sh new file mode 100644 index 00000000..cf7d1f98 --- /dev/null +++ b/playbooks/helpers.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash + +# +# Shared Playbook Helpers +# ---- +# Common bash functions used across multiple playbooks. +# Source this file at the top of playbooks that need these functions: +# source "$(dirname "$0")/helpers.sh" +# + +# ---- +# Permission Management +# ---- + +# +# Execute command with appropriate permissions + +run_cmd() { + if [[ $DEPLOYER_PERMS == 'root' ]]; then + "$@" + else + sudo -n "$@" + fi +} + +# ---- +# PHP Detection +# ---- + +# +# Detect default PHP version + +detect_php_default() { + local default_version + + # Try update-alternatives first + if command -v update-alternatives > /dev/null 2>&1; then + default_version=$(update-alternatives --query php 2> /dev/null | grep '^Value:' | awk '{print $2}') + if [[ -n $default_version && $default_version =~ php([0-9]+\.[0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return + fi + fi + + # Fallback: check /usr/bin/php directly + if [[ -x /usr/bin/php ]]; then + default_version=$(/usr/bin/php -v 2> /dev/null | head -n1 | grep -oP 'PHP \K[0-9]+\.[0-9]+') + if [[ -n $default_version ]]; then + echo "$default_version" + return + fi + fi +} + +# ---- +# Package Management +# ---- + +# +# Wait for dpkg lock to be released + +wait_for_dpkg_lock() { + local max_wait=60 + local waited=0 + local lock_found=false + + # Check multiple times to catch the lock even in race conditions + while ((waited < max_wait)); do + # Try to acquire the lock by checking if we can open it + if fuser /var/lib/dpkg/lock-frontend > /dev/null 2>&1 \ + || fuser /var/lib/dpkg/lock > /dev/null 2>&1 \ + || fuser /var/lib/apt/lists/lock > /dev/null 2>&1; then + lock_found=true + echo "✓ Waiting for package manager lock to be released..." + sleep 2 + waited=$((waited + 2)) + else + # Lock not held, but wait a bit to ensure it's really released + if [[ $lock_found == true ]]; then + # Was locked before, give it extra time + sleep 2 + else + # Never saw lock, just a small delay + sleep 1 + fi + return 0 + fi + done + + echo "Error: Timeout waiting for dpkg lock to be released" >&2 + return 1 +} + +# +# apt-get with retry + +apt_get_with_retry() { + local max_attempts=5 + local attempt=1 + local wait_time=10 + local output + + while ((attempt <= max_attempts)); do + # Capture output to check for lock errors + output=$(run_cmd apt-get "$@" 2>&1) + local exit_code=$? + + if ((exit_code == 0)); then + [[ -n $output ]] && echo "$output" + return 0 + fi + + # Only retry on lock-related errors + if echo "$output" | grep -qE 'Could not get lock|dpkg.*lock|Unable to acquire'; then + if ((attempt < max_attempts)); then + echo "✓ Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." + sleep "$wait_time" + wait_time=$((wait_time + 5)) + attempt=$((attempt + 1)) + wait_for_dpkg_lock || true + else + echo "$output" >&2 + return "$exit_code" + fi + else + # Non-lock error, fail immediately + echo "$output" >&2 + return "$exit_code" + fi + done + + return 1 +} diff --git a/playbooks/server-info.sh b/playbooks/server-info.sh index d875bad9..6abf2331 100755 --- a/playbooks/server-info.sh +++ b/playbooks/server-info.sh @@ -26,6 +26,9 @@ if [[ -z $DEPLOYER_OUTPUT_FILE ]]; then exit 1 fi +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + # ---- # Detection Functions # ---- @@ -119,21 +122,6 @@ check_permissions() { # Helper Functions # ---- -# -# Command Execution -# ---- - -# -# Execute command with appropriate permissions - -run_cmd() { - if [[ $DEPLOYER_PERMS == 'root' ]]; then - "$@" - else - sudo -n "$@" - fi -} - # # Tool Installation # ---- @@ -230,31 +218,6 @@ detect_php_versions() { fi } -# -# Detect default PHP version - -detect_php_default() { - local default_version - - # Try update-alternatives first - if command -v update-alternatives > /dev/null 2>&1; then - default_version=$(update-alternatives --query php 2> /dev/null | grep '^Value:' | awk '{print $2}') - if [[ -n $default_version && $default_version =~ php([0-9]+\.[0-9]+)$ ]]; then - echo "${BASH_REMATCH[1]}" - return - fi - fi - - # Fallback: check /usr/bin/php directly - if [[ -x /usr/bin/php ]]; then - default_version=$(/usr/bin/php -v 2> /dev/null | head -n1 | grep -oP 'PHP \K[0-9]+\.[0-9]+') - if [[ -n $default_version ]]; then - echo "$default_version" - return - fi - fi -} - # ---- # Service Metrics # ---- diff --git a/playbooks/server-install-php.sh b/playbooks/server-install-php.sh index 66dea6da..29e43f0b 100644 --- a/playbooks/server-install-php.sh +++ b/playbooks/server-install-php.sh @@ -34,104 +34,8 @@ export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_PHP_SET_DEFAULT ]] && echo "Error: DEPLOYER_PHP_SET_DEFAULT required" && exit 1 export DEPLOYER_PERMS -# ---- -# Helpers -# ---- - -# -# Permission Management -# ---- - -# -# Execute command with appropriate permissions - -run_cmd() { - if [[ $DEPLOYER_PERMS == 'root' ]]; then - "$@" - else - sudo -n "$@" - fi -} - -# -# Package Management -# ---- - -# -# Wait for dpkg lock to be released - -wait_for_dpkg_lock() { - local max_wait=60 - local waited=0 - local lock_found=false - - # Check multiple times to catch the lock even in race conditions - while ((waited < max_wait)); do - # Try to acquire the lock by checking if we can open it - if fuser /var/lib/dpkg/lock-frontend > /dev/null 2>&1 \ - || fuser /var/lib/dpkg/lock > /dev/null 2>&1 \ - || fuser /var/lib/apt/lists/lock > /dev/null 2>&1; then - lock_found=true - echo "✓ Waiting for package manager lock to be released..." - sleep 2 - waited=$((waited + 2)) - else - # Lock not held, but wait a bit to ensure it's really released - if [[ $lock_found == true ]]; then - # Was locked before, give it extra time - sleep 2 - else - # Never saw lock, just a small delay - sleep 1 - fi - return 0 - fi - done - - echo "Error: Timeout waiting for dpkg lock to be released" >&2 - return 1 -} - -# -# apt-get with retry - -apt_get_with_retry() { - local max_attempts=5 - local attempt=1 - local wait_time=10 - local output - - while ((attempt <= max_attempts)); do - # Capture output to check for lock errors - output=$(run_cmd apt-get "$@" 2>&1) - local exit_code=$? - - if ((exit_code == 0)); then - [[ -n $output ]] && echo "$output" - return 0 - fi - - # Only retry on lock-related errors - if echo "$output" | grep -qE 'Could not get lock|dpkg.*lock|Unable to acquire'; then - if ((attempt < max_attempts)); then - echo "✓ Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." - sleep "$wait_time" - wait_time=$((wait_time + 5)) - attempt=$((attempt + 1)) - wait_for_dpkg_lock || true - else - echo "$output" >&2 - return "$exit_code" - fi - else - # Non-lock error, fail immediately - echo "$output" >&2 - return "$exit_code" - fi - done - - return 1 -} +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" # ---- # Installation Functions diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index b55fc37c..58aba5b8 100644 --- a/playbooks/server-install.sh +++ b/playbooks/server-install.sh @@ -36,104 +36,8 @@ export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_SERVER_NAME ]] && echo "Error: DEPLOYER_SERVER_NAME required" && exit 1 export DEPLOYER_PERMS -# ---- -# Helpers -# ---- - -# -# Permission Management -# ---- - -# -# Execute command with appropriate permissions - -run_cmd() { - if [[ $DEPLOYER_PERMS == 'root' ]]; then - "$@" - else - sudo -n "$@" - fi -} - -# -# Package Management -# ---- - -# -# Wait for dpkg lock to be released - -wait_for_dpkg_lock() { - local max_wait=60 - local waited=0 - local lock_found=false - - # Check multiple times to catch the lock even in race conditions - while ((waited < max_wait)); do - # Try to acquire the lock by checking if we can open it - if fuser /var/lib/dpkg/lock-frontend > /dev/null 2>&1 \ - || fuser /var/lib/dpkg/lock > /dev/null 2>&1 \ - || fuser /var/lib/apt/lists/lock > /dev/null 2>&1; then - lock_found=true - echo "✓ Waiting for package manager lock to be released..." - sleep 2 - waited=$((waited + 2)) - else - # Lock not held, but wait a bit to ensure it's really released - if [[ $lock_found == true ]]; then - # Was locked before, give it extra time - sleep 2 - else - # Never saw lock, just a small delay - sleep 1 - fi - return 0 - fi - done - - echo "Error: Timeout waiting for dpkg lock to be released" >&2 - return 1 -} - -# -# apt-get with retry - -apt_get_with_retry() { - local max_attempts=5 - local attempt=1 - local wait_time=10 - local output - - while ((attempt <= max_attempts)); do - # Capture output to check for lock errors - output=$(run_cmd apt-get "$@" 2>&1) - local exit_code=$? - - if ((exit_code == 0)); then - [[ -n $output ]] && echo "$output" - return 0 - fi - - # Only retry on lock-related errors - if echo "$output" | grep -qE 'Could not get lock|dpkg.*lock|Unable to acquire'; then - if ((attempt < max_attempts)); then - echo "✓ Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." - sleep "$wait_time" - wait_time=$((wait_time + 5)) - attempt=$((attempt + 1)) - wait_for_dpkg_lock || true - else - echo "$output" >&2 - return "$exit_code" - fi - else - # Non-lock error, fail immediately - echo "$output" >&2 - return "$exit_code" - fi - done - - return 1 -} +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" # ---- # Installation Functions From 84ca43e02b6906f82c7d4d5366f96ae1734e5c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Tue, 11 Nov 2025 13:23:02 +0200 Subject: [PATCH 3/3] feat(playbooks): separate PHP user group config and improve Caddy management - Move PHP-FPM user group configuration from server-install.sh to server-install-php.sh - Implement marker-based Caddy localhost config management - Improve separation of concerns between server and PHP installation playbooks --- playbooks/server-install-php.sh | 61 ++++++++++++++++++++++++--------- playbooks/server-install.sh | 27 +++------------ 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/playbooks/server-install-php.sh b/playbooks/server-install-php.sh index 29e43f0b..32a45cb6 100644 --- a/playbooks/server-install-php.sh +++ b/playbooks/server-install-php.sh @@ -165,6 +165,37 @@ configure_php_fpm() { fi } +# +# User Group Configuration +# ---- + +# +# Configure PHP-FPM user group membership for file access + +configure_php_user_groups() { + # Add www-data (PHP-FPM user) to deployer group so it can access files + if id -u www-data > /dev/null 2>&1; then + if ! id -nG www-data 2> /dev/null | grep -qw deployer; then + echo "✓ Adding www-data user to deployer group..." + if ! run_cmd usermod -aG deployer www-data; then + echo "Error: Failed to add www-data to deployer group" >&2 + exit 1 + fi + + # Restart PHP-FPM so it picks up the new group membership + if systemctl is-active --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then + echo "✓ Restarting PHP-FPM to apply group membership..." + if ! run_cmd systemctl restart php${DEPLOYER_PHP_VERSION}-fpm; then + echo "Error: Failed to restart PHP-FPM" >&2 + exit 1 + fi + fi + fi + else + echo "Warning: PHP-FPM user 'www-data' not found, skipping group assignment" + fi +} + # # Default Version Configuration # ---- @@ -210,30 +241,24 @@ update_caddy_config() { echo "✓ Updating Caddy localhost configuration..." - # Check if the base server block exists - if ! grep -q "http://localhost:9001" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then - # Create base server block structure - if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then - # PHP-FPM status endpoints - localhost only (not accessible from internet) - http://localhost:9001 { - } - EOF - echo "Error: Failed to create Caddy localhost configuration" >&2 - exit 1 - fi - fi - # Check if this PHP version's endpoint already exists if grep -q "handle_path /php${DEPLOYER_PHP_VERSION}/" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then echo "✓ PHP ${DEPLOYER_PHP_VERSION} endpoint already configured" return 0 fi + # Check if the marker exists (file should be created by server-install.sh) + if ! grep -q "#### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :) ####" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then + echo "Error: localhost.caddy marker not found. File may have been modified manually." >&2 + exit 1 + fi + # Create temporary file with the new handle block local temp_handle temp_handle=$(mktemp) cat > "$temp_handle" <<- EOF + handle_path /php${DEPLOYER_PHP_VERSION}/* { reverse_proxy unix//run/php/php${DEPLOYER_PHP_VERSION}-fpm.sock { transport fastcgi { @@ -244,15 +269,15 @@ update_caddy_config() { } EOF - # Insert the handle block before the closing brace of the server block + # Insert the handle block after the marker local temp_config temp_config=$(mktemp) - # Read the file, insert handle block before last closing brace if ! awk -v handle="$(cat "$temp_handle")" ' - /^}$/ && !found { + /#### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :\) ####/ { + print print handle - found=1 + next } { print } ' /etc/caddy/conf.d/localhost.caddy > "$temp_config"; then @@ -290,6 +315,7 @@ main() { setup_php_repository install_php_packages configure_php_fpm + configure_php_user_groups set_as_default update_caddy_config @@ -311,6 +337,7 @@ main() { - setup_php_repository - install_php_packages - configure_php_fpm + - configure_php_user_groups - set_as_default - update_caddy_config EOF diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index 58aba5b8..2f49e83b 100644 --- a/playbooks/server-install.sh +++ b/playbooks/server-install.sh @@ -186,8 +186,10 @@ setup_caddy_structure() { # Create localhost.caddy - monitoring endpoints only accessible via localhost # (PHP-FPM status endpoint will be added by PHP installation playbook) if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then - # Localhost-only endpoints configuration - # PHP-FPM status endpoint will be configured during PHP installation + # PHP-FPM status endpoints - localhost only (not accessible from internet) + http://localhost:9001 { + #### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :) #### + } EOF echo "Error: Failed to create localhost.caddy" >&2 exit 1 @@ -236,27 +238,6 @@ configure_deployer_groups() { fi fi - # Add www-data (PHP-FPM user) to deployer group so it can access files - if id -u www-data > /dev/null 2>&1; then - if ! id -nG www-data 2> /dev/null | grep -qw deployer; then - echo "✓ Adding www-data user to deployer group..." - if ! run_cmd usermod -aG deployer www-data; then - echo "Error: Failed to add www-data to deployer group" >&2 - exit 1 - fi - - # Restart PHP-FPM so it picks up the new group membership - if systemctl is-active --quiet php8.4-fpm 2> /dev/null; then - echo "✓ Restarting PHP-FPM to apply group membership..." - if ! run_cmd systemctl restart php8.4-fpm; then - echo "Error: Failed to restart PHP-FPM" >&2 - exit 1 - fi - fi - fi - else - echo "Warning: PHP-FPM user 'www-data' not found, skipping group assignment" - fi } #