Skip to content

Commit a633e40

Browse files
Merge pull request #41 from move-elevator/feat/add-disk-space-check-with-low-space-warning
feat: add disk space check to requirements recipe
2 parents 3227837 + 78fb032 commit a633e40

8 files changed

Lines changed: 184 additions & 10 deletions

File tree

autoload.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
require_once(__DIR__ . '/deployer/requirements/task/check_user.php');
5959
require_once(__DIR__ . '/deployer/requirements/task/check_env.php');
6060
require_once(__DIR__ . '/deployer/requirements/task/check_eol.php');
61+
require_once(__DIR__ . '/deployer/requirements/task/check_disk_space.php');
6162
require_once(__DIR__ . '/deployer/requirements/task/list.php');
6263
require_once(__DIR__ . '/deployer/requirements/task/health.php');
6364

deployer/requirements/config/set.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
set('requirements_check_eol_enabled', true);
2121
set('requirements_check_database_grants_enabled', true);
2222
set('requirements_check_health_enabled', true);
23+
set('requirements_check_disk_space_enabled', true);
2324

2425
// Locales
2526
set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']);
@@ -113,6 +114,13 @@
113114
// Health check
114115
set('requirements_health_url', 'http://localhost');
115116

117+
// Disk space (percent used; webspace path defaults to the deploy path)
118+
set('requirements_disk_space_warn_percent', 80);
119+
set('requirements_disk_space_fail_percent', 95);
120+
set('requirements_disk_space_webspace_path', function (): string {
121+
return has('deploy_path') ? get('deploy_path') : '.';
122+
});
123+
116124
// User / permissions
117125
set('requirements_user_group', 'www-data');
118126
set('requirements_deploy_path_permissions', '2770');

deployer/requirements/functions.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,77 @@ function resolveDatabaseCredentials(): ?array
517517
];
518518
}
519519

520+
/**
521+
* Run a query against the remote database via the `mysql`/`mariadb` CLI client, with the password masked in output.
522+
*
523+
* @param array{user: string, password: string, host: string, port: int} $credentials
524+
*
525+
* @throws RunException if the connection or query fails
526+
*/
527+
function runMysqlQuery(array $credentials, string $query): string
528+
{
529+
$mysqlBin = has('mysql') ? get('mysql') : 'mysql';
530+
531+
return run(sprintf(
532+
'%s --connect-timeout=5 -u %s -p%s -h %s -P %d -N -e %s 2>&1',
533+
escapeshellarg($mysqlBin),
534+
escapeshellarg($credentials['user']),
535+
"'%secret%'",
536+
escapeshellarg($credentials['host']),
537+
$credentials['port'],
538+
escapeshellarg($query)
539+
), secret: $credentials['password']);
540+
}
541+
542+
/**
543+
* Run `df` against a path on the remote host and record a requirement row based on used-space thresholds.
544+
*/
545+
function checkDiskSpaceAtPath(string $label, string $path, int $warnPercent, int $failPercent): void
546+
{
547+
try {
548+
$output = trim(run('df -kP ' . escapeshellarg($path) . ' | tail -n 1'));
549+
} catch (RunException) {
550+
addRequirementRow($label, REQUIREMENT_SKIP, "Could not read disk usage for $path");
551+
552+
return;
553+
}
554+
555+
$columns = preg_split('/\s+/', $output);
556+
557+
if (!is_array($columns) || count($columns) < 5) {
558+
addRequirementRow($label, REQUIREMENT_SKIP, "Unexpected df output for $path");
559+
560+
return;
561+
}
562+
563+
$usedPercent = (int) rtrim($columns[4], '%');
564+
$availableHuman = formatKilobytes((int) $columns[3]);
565+
566+
$status = match (true) {
567+
$usedPercent >= $failPercent => REQUIREMENT_FAIL,
568+
$usedPercent >= $warnPercent => REQUIREMENT_WARN,
569+
default => REQUIREMENT_OK,
570+
};
571+
572+
addRequirementRow($label, $status, "{$usedPercent}% used, $availableHuman free ($path)");
573+
}
574+
575+
/**
576+
* Format a `df -k` kilobyte value as a human-readable size.
577+
*/
578+
function formatKilobytes(int $kilobytes): string
579+
{
580+
if ($kilobytes >= 1024 * 1024) {
581+
return round($kilobytes / (1024 * 1024), 1) . 'G';
582+
}
583+
584+
if ($kilobytes >= 1024) {
585+
return round($kilobytes / 1024, 1) . 'M';
586+
}
587+
588+
return $kilobytes . 'K';
589+
}
590+
520591
/**
521592
* Parse SHOW GRANTS output and check required grants on global level (*.*).
522593
*

deployer/requirements/task/check_database_grants.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,10 @@ function checkRootGrants(): void
4949
return;
5050
}
5151

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

5554
try {
56-
$output = run(sprintf(
57-
'%s --connect-timeout=5 -u %s -p%s -h %s -P %d -N -e %s 2>&1',
58-
escapeshellarg($mysqlBin),
59-
escapeshellarg($credentials['user']),
60-
"'%secret%'",
61-
escapeshellarg($credentials['host']),
62-
$credentials['port'],
63-
escapeshellarg('SHOW GRANTS FOR CURRENT_USER()')
64-
), secret: $credentials['password']);
55+
$output = runMysqlQuery($credentials, 'SHOW GRANTS FOR CURRENT_USER()');
6556
} catch (RunException) {
6657
addRequirementRow('Database: connectivity', REQUIREMENT_FAIL, "Cannot connect as $connectInfo");
6758

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Deployer;
6+
7+
use Deployer\Exception\RunException;
8+
9+
task('requirements:check:disk_space', function (): void {
10+
if (!get('requirements_check_disk_space_enabled')) {
11+
return;
12+
}
13+
14+
$warnPercent = (int) get('requirements_disk_space_warn_percent');
15+
$failPercent = (int) get('requirements_disk_space_fail_percent');
16+
17+
checkDiskSpaceAtPath('Disk space: webspace', get('requirements_disk_space_webspace_path'), $warnPercent, $failPercent);
18+
19+
$credentials = resolveDatabaseCredentials();
20+
21+
if (null === $credentials) {
22+
addRequirementRow('Disk space: database', REQUIREMENT_SKIP, 'No database credentials available');
23+
24+
return;
25+
}
26+
27+
if (!in_array($credentials['host'], ['127.0.0.1', 'localhost'], true)) {
28+
addRequirementRow(
29+
'Disk space: database',
30+
REQUIREMENT_SKIP,
31+
"Database host ({$credentials['host']}) not reachable from the deploy target"
32+
);
33+
34+
return;
35+
}
36+
37+
$datadir = detectDatabaseDatadir($credentials);
38+
39+
if (null === $datadir) {
40+
addRequirementRow('Disk space: database', REQUIREMENT_SKIP, 'Could not determine database data directory');
41+
42+
return;
43+
}
44+
45+
checkDiskSpaceAtPath('Disk space: database', $datadir, $warnPercent, $failPercent);
46+
})->hidden();
47+
48+
/**
49+
* @param array{user: string, password: string, host: string, port: int} $credentials
50+
*/
51+
function detectDatabaseDatadir(array $credentials): ?string
52+
{
53+
try {
54+
$output = trim(runMysqlQuery($credentials, "SHOW VARIABLES LIKE 'datadir'"));
55+
} catch (RunException) {
56+
return null;
57+
}
58+
59+
// Output format: "datadir\t/var/lib/mysql/", possibly preceded by mysql client warning lines
60+
// on stderr (merged into stdout by runMysqlQuery()), so only the last line is relevant.
61+
$lines = explode("\n", $output);
62+
$columns = explode("\t", trim((string) end($lines)));
63+
$path = $columns[1] ?? null;
64+
65+
return ('' !== $path && null !== $path) ? $path : null;
66+
}

deployer/requirements/task/list.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,14 @@
134134
}
135135
}
136136

137+
// Disk Space
138+
if (get('requirements_check_disk_space_enabled')) {
139+
writeln('');
140+
writeln('<fg=yellow;options=bold>Disk Space</>');
141+
writeln(sprintf(' Warn: >= %d%% used', (int) get('requirements_disk_space_warn_percent')));
142+
writeln(sprintf(' Fail: >= %d%% used', (int) get('requirements_disk_space_fail_percent')));
143+
writeln(' Checked: webspace path, and database data directory when the DB host is local');
144+
}
145+
137146
writeln('');
138147
})->desc('List server requirements');

deployer/requirements/task/requirements.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'requirements:check:user',
1717
'requirements:check:env',
1818
'requirements:check:eol',
19+
'requirements:check:disk_space',
1920
'requirements:check:summary',
2021
])->desc('Check server requirements');
2122

docs/REQUIREMENTS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflif
120120

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

123+
### Disk space
124+
125+
Checks free disk space (via `df`) against two used-space thresholds:
126+
127+
| Condition | Status |
128+
|-----------|--------|
129+
| Used space >= fail threshold (default: 95%) | FAIL |
130+
| Used space >= warn threshold (default: 80%) | WARN |
131+
| Otherwise | OK |
132+
133+
Two locations are checked:
134+
135+
- **Webspace**: the configured `requirements_disk_space_webspace_path` (defaults to `deploy_path`).
136+
- **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.
137+
138+
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.:
139+
140+
```php
141+
before('deploy:prepare', 'requirements:check:disk_space');
142+
```
143+
123144
## Health check
124145

125146
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.
@@ -201,6 +222,12 @@ set('requirements_check_database_grants_enabled', true);
201222
// Health check
202223
set('requirements_check_health_enabled', true);
203224
set('requirements_health_url', 'https://example.com');
225+
226+
// Disk space check
227+
set('requirements_check_disk_space_enabled', true);
228+
set('requirements_disk_space_warn_percent', 80);
229+
set('requirements_disk_space_fail_percent', 95);
230+
set('requirements_disk_space_webspace_path', '/var/www/html');
204231
```
205232

206233
## Extending with custom checks

0 commit comments

Comments
 (0)