Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
require_once(__DIR__ . '/deployer/requirements/task/check_user.php');
require_once(__DIR__ . '/deployer/requirements/task/check_env.php');
require_once(__DIR__ . '/deployer/requirements/task/check_eol.php');
require_once(__DIR__ . '/deployer/requirements/task/check_disk_space.php');
require_once(__DIR__ . '/deployer/requirements/task/list.php');
require_once(__DIR__ . '/deployer/requirements/task/health.php');

Expand Down
8 changes: 8 additions & 0 deletions deployer/requirements/config/set.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
set('requirements_check_eol_enabled', true);
set('requirements_check_database_grants_enabled', true);
set('requirements_check_health_enabled', true);
set('requirements_check_disk_space_enabled', true);

// Locales
set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']);
Expand Down Expand Up @@ -113,6 +114,13 @@
// Health check
set('requirements_health_url', 'http://localhost');

// Disk space (percent used; webspace path defaults to the deploy path)
set('requirements_disk_space_warn_percent', 80);
set('requirements_disk_space_fail_percent', 95);
set('requirements_disk_space_webspace_path', function (): string {
return has('deploy_path') ? get('deploy_path') : '.';
});

// User / permissions
set('requirements_user_group', 'www-data');
set('requirements_deploy_path_permissions', '2770');
Expand Down
71 changes: 71 additions & 0 deletions deployer/requirements/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,77 @@ function resolveDatabaseCredentials(): ?array
];
}

/**
* Run a query against the remote database via the `mysql`/`mariadb` CLI client, with the password masked in output.
*
* @param array{user: string, password: string, host: string, port: int} $credentials
*
* @throws RunException if the connection or query fails
*/
function runMysqlQuery(array $credentials, string $query): string
{
$mysqlBin = has('mysql') ? get('mysql') : 'mysql';

return run(sprintf(
'%s --connect-timeout=5 -u %s -p%s -h %s -P %d -N -e %s 2>&1',
escapeshellarg($mysqlBin),
escapeshellarg($credentials['user']),
"'%secret%'",
escapeshellarg($credentials['host']),
$credentials['port'],
escapeshellarg($query)
), secret: $credentials['password']);
}

/**
* Run `df` against a path on the remote host and record a requirement row based on used-space thresholds.
*/
function checkDiskSpaceAtPath(string $label, string $path, int $warnPercent, int $failPercent): void
{
try {
$output = trim(run('df -kP ' . escapeshellarg($path) . ' | tail -n 1'));
} catch (RunException) {
addRequirementRow($label, REQUIREMENT_SKIP, "Could not read disk usage for $path");

return;
}

$columns = preg_split('/\s+/', $output);

if (!is_array($columns) || count($columns) < 5) {
addRequirementRow($label, REQUIREMENT_SKIP, "Unexpected df output for $path");

return;
}

$usedPercent = (int) rtrim($columns[4], '%');
$availableHuman = formatKilobytes((int) $columns[3]);

$status = match (true) {
$usedPercent >= $failPercent => REQUIREMENT_FAIL,
$usedPercent >= $warnPercent => REQUIREMENT_WARN,
default => REQUIREMENT_OK,
};

addRequirementRow($label, $status, "{$usedPercent}% used, $availableHuman free ($path)");
}

/**
* Format a `df -k` kilobyte value as a human-readable size.
*/
function formatKilobytes(int $kilobytes): string
{
if ($kilobytes >= 1024 * 1024) {
return round($kilobytes / (1024 * 1024), 1) . 'G';
}

if ($kilobytes >= 1024) {
return round($kilobytes / 1024, 1) . 'M';
}

return $kilobytes . 'K';
}

/**
* Parse SHOW GRANTS output and check required grants on global level (*.*).
*
Expand Down
11 changes: 1 addition & 10 deletions deployer/requirements/task/check_database_grants.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,10 @@ function checkRootGrants(): void
return;
}

$mysqlBin = has('mysql') ? get('mysql') : 'mysql';
$connectInfo = sprintf('%s@%s:%d', $credentials['user'], $credentials['host'], $credentials['port']);

try {
$output = run(sprintf(
'%s --connect-timeout=5 -u %s -p%s -h %s -P %d -N -e %s 2>&1',
escapeshellarg($mysqlBin),
escapeshellarg($credentials['user']),
"'%secret%'",
escapeshellarg($credentials['host']),
$credentials['port'],
escapeshellarg('SHOW GRANTS FOR CURRENT_USER()')
), secret: $credentials['password']);
$output = runMysqlQuery($credentials, 'SHOW GRANTS FOR CURRENT_USER()');
} catch (RunException) {
addRequirementRow('Database: connectivity', REQUIREMENT_FAIL, "Cannot connect as $connectInfo");

Expand Down
66 changes: 66 additions & 0 deletions deployer/requirements/task/check_disk_space.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Deployer;

use Deployer\Exception\RunException;

task('requirements:check:disk_space', function (): void {
if (!get('requirements_check_disk_space_enabled')) {
return;
}

$warnPercent = (int) get('requirements_disk_space_warn_percent');
$failPercent = (int) get('requirements_disk_space_fail_percent');

checkDiskSpaceAtPath('Disk space: webspace', get('requirements_disk_space_webspace_path'), $warnPercent, $failPercent);

$credentials = resolveDatabaseCredentials();

if (null === $credentials) {
addRequirementRow('Disk space: database', REQUIREMENT_SKIP, 'No database credentials available');

return;
}

if (!in_array($credentials['host'], ['127.0.0.1', 'localhost'], true)) {
addRequirementRow(
'Disk space: database',
REQUIREMENT_SKIP,
"Database host ({$credentials['host']}) not reachable from the deploy target"
);

return;
}

$datadir = detectDatabaseDatadir($credentials);

if (null === $datadir) {
addRequirementRow('Disk space: database', REQUIREMENT_SKIP, 'Could not determine database data directory');

return;
}

checkDiskSpaceAtPath('Disk space: database', $datadir, $warnPercent, $failPercent);
})->hidden();

/**
* @param array{user: string, password: string, host: string, port: int} $credentials
*/
function detectDatabaseDatadir(array $credentials): ?string
{
try {
$output = trim(runMysqlQuery($credentials, "SHOW VARIABLES LIKE 'datadir'"));
} catch (RunException) {
return null;
}

// Output format: "datadir\t/var/lib/mysql/", possibly preceded by mysql client warning lines
// on stderr (merged into stdout by runMysqlQuery()), so only the last line is relevant.
$lines = explode("\n", $output);
$columns = explode("\t", trim((string) end($lines)));
$path = $columns[1] ?? null;

return ('' !== $path && null !== $path) ? $path : null;
}
9 changes: 9 additions & 0 deletions deployer/requirements/task/list.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,14 @@
}
}

// Disk Space
if (get('requirements_check_disk_space_enabled')) {
writeln('');
writeln('<fg=yellow;options=bold>Disk Space</>');
writeln(sprintf(' Warn: >= %d%% used', (int) get('requirements_disk_space_warn_percent')));
writeln(sprintf(' Fail: >= %d%% used', (int) get('requirements_disk_space_fail_percent')));
writeln(' Checked: webspace path, and database data directory when the DB host is local');
}

writeln('');
})->desc('List server requirements');
1 change: 1 addition & 0 deletions deployer/requirements/task/requirements.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'requirements:check:user',
'requirements:check:env',
'requirements:check:eol',
'requirements:check:disk_space',
'requirements:check:summary',
])->desc('Check server requirements');

Expand Down
27 changes: 27 additions & 0 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflif

The warning threshold is configurable (default: 6 months before EOL).

### Disk space

Checks free disk space (via `df`) against two used-space thresholds:

| Condition | Status |
|-----------|--------|
| Used space >= fail threshold (default: 95%) | FAIL |
| Used space >= warn threshold (default: 80%) | WARN |
| Otherwise | OK |

Two locations are checked:

- **Webspace**: the configured `requirements_disk_space_webspace_path` (defaults to `deploy_path`).
- **Database**: only when the resolved database host (see "Database grants" credential resolution above) is `127.0.0.1`/`localhost`, i.e. reachable from the deploy target. The data directory is read via `SHOW VARIABLES LIKE 'datadir'`. If the database lives on a separate host, this check is skipped, since disk usage on an arbitrary remote host cannot be read via SSH from the deploy target.

Like every other check in this recipe, FAIL/WARN are reported in the summary table only — they do not stop the `requirements:check` command itself. To block a deployment on disk space, wire the check into the deploy pipeline in the consuming project, e.g.:

```php
before('deploy:prepare', 'requirements:check:disk_space');
```

## 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.
Expand Down Expand Up @@ -201,6 +222,12 @@ set('requirements_check_database_grants_enabled', true);
// Health check
set('requirements_check_health_enabled', true);
set('requirements_health_url', 'https://example.com');

// Disk space check
set('requirements_check_disk_space_enabled', true);
set('requirements_disk_space_warn_percent', 80);
set('requirements_disk_space_fail_percent', 95);
set('requirements_disk_space_webspace_path', '/var/www/html');
```

## Extending with custom checks
Expand Down