|
#!/usr/bin/env php
|
|
<?php
|
|
|
|
declare(strict_types = 1);
|
|
|
|
/*
|
|
* Functions section
|
|
*/
|
|
|
|
$apiToken = NULL;
|
|
$silent = !in_array('-v', $argv);
|
|
$newToken = in_array('-t', $argv);
|
|
|
|
function readableSize(float $in) : string {
|
|
// Bytes to Tebibyte, rounded to 2 digits
|
|
return (string) round($in / 1.1e+12, 2) . 'TB';
|
|
}
|
|
|
|
function logStr(string $message, bool $force = FALSE) : void {
|
|
|
|
global $silent;
|
|
|
|
if (!$silent || $force) {
|
|
echo date('H:i:s') . ' ' . $message . PHP_EOL;
|
|
}
|
|
|
|
}
|
|
|
|
function exitWithStatus(string $severity, string $message) : void {
|
|
|
|
$exitCodeMap = [
|
|
'OK' => 0,
|
|
'WARNING' => 1,
|
|
'CRITICAL' => 2,
|
|
'UNKNOWN' => 3,
|
|
];
|
|
|
|
$exitCode = $exitCodeMap[$severity] ?? NULL;
|
|
if ($exitCode === NULL) {
|
|
exitWithStatus('UNKNOWN', 'Unknown severity "' . $severity . '"');
|
|
}
|
|
|
|
echo($severity . ' - ' . $message . PHP_EOL);
|
|
|
|
exit($exitCode);
|
|
}
|
|
|
|
function createBasicCURLHandle(string $url, array $additionalHeaders = []) {
|
|
|
|
global $apiEndpoint, $apiPort, $apiUsername, $apiPassword, $apiToken, $apiVerifySSL, $apiTokenCachePath;
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, 'https://' . $apiEndpoint . $url);
|
|
curl_setopt($ch, CURLOPT_PORT, $apiPort);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(['Content-Type: application/json', 'Accept: application/vnd.ceph.api.v1.0+json'], $additionalHeaders));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
|
|
if (!$apiVerifySSL) {
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
|
}
|
|
|
|
return $ch;
|
|
}
|
|
|
|
function checkCurlResponse($ch) : void {
|
|
|
|
$info = curl_getinfo($ch);
|
|
|
|
$apiStr = parse_url($info['url'], PHP_URL_PATH);
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
if ($httpCode === 0) {
|
|
curl_close($ch);
|
|
exitWithStatus('UNKNOWN', 'Got CURL error for endpoint "' . $apiStr . '": ' . curl_error($ch));
|
|
} elseif ($httpCode < 200 || $httpCode >= 300) {
|
|
curl_close($ch);
|
|
exitWithStatus('UNKNOWN', 'Unexpected HTTP code from API endpoint "' . $apiStr . '": ' . $httpCode);
|
|
}
|
|
|
|
}
|
|
|
|
function getAPIToken() : string {
|
|
|
|
global $apiEndpoint, $apiUsername, $apiPassword, $apiToken, $apiTokenCachePath;
|
|
|
|
if ($apiToken === NULL) {
|
|
|
|
if (file_exists($apiTokenCachePath)) {
|
|
|
|
$tokenDataStr = file_get_contents($apiTokenCachePath);
|
|
$tokenData = json_decode($tokenDataStr);
|
|
|
|
// The token needs to be valid at least for 10 more seconds
|
|
if ($tokenData->validUntil > time() + 10) {
|
|
$apiToken = $tokenData->token;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($apiToken === NULL) {
|
|
|
|
logStr('Retrieving new API token');
|
|
|
|
$ch = createBasicCURLHandle('/api/auth');
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
'username' => $apiUsername,
|
|
'password' => $apiPassword,
|
|
]));
|
|
|
|
$jsonStr = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
checkCurlResponse($ch);
|
|
curl_close($ch);
|
|
|
|
$payload = json_decode($jsonStr);
|
|
|
|
$apiToken = $payload->token;
|
|
|
|
file_put_contents($apiTokenCachePath, json_encode([
|
|
'validUntil' => time() + (60 * 60 * 8), // Todo: Read from JWT instead, here we assume 8 hours
|
|
'token' => $apiToken,
|
|
]));
|
|
chmod($apiTokenCachePath, 0600);
|
|
|
|
}
|
|
|
|
return $apiToken;
|
|
}
|
|
|
|
function doGet(string $url) {
|
|
|
|
global $apiEndpoint;
|
|
|
|
$ch = createBasicCURLHandle($url, ['Authorization: Bearer ' . getAPIToken()]);
|
|
|
|
$jsonStr = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
checkCurlResponse($ch);
|
|
curl_close($ch);
|
|
|
|
return json_decode($jsonStr);
|
|
}
|
|
|
|
/*
|
|
* End functions section
|
|
*
|
|
* Loading config
|
|
*/
|
|
|
|
$configFile = '/etc/nrpe-ceph.json';
|
|
|
|
if (!file_exists($configFile)) {
|
|
exitWithStatus('UNKNOWN', 'Config file "' . $configFile . '" not found');
|
|
}
|
|
|
|
$configData = json_decode(file_get_contents($configFile), TRUE);
|
|
|
|
$warningPercentageOffset = (int) ($configData['warningPercentageOffset'] ?? 5);
|
|
$allowedNodeFailures = (int) ($configData['allowedNodeFailures'] ?? 1);
|
|
|
|
$apiEndpoint = $configData['api']['host'];
|
|
$apiPort = $configData['api']['port'] ?? 8443;
|
|
$apiUsername = $configData['api']['username'];
|
|
$apiPassword = $configData['api']['password'];
|
|
$apiVerifySSL = $configData['api']['verifySSL'] ?? TRUE;
|
|
$apiTokenCachePath = $configData['api']['tokenCachePath'] ?? '/tmp/nrpe-ceph-token';
|
|
|
|
/*
|
|
* End of config loading
|
|
*/
|
|
|
|
if ($newToken && file_exists($apiTokenCachePath)) {
|
|
|
|
logStr('Removing old cached');
|
|
unlink($apiTokenCachePath);
|
|
|
|
}
|
|
|
|
logStr('Loading data..');
|
|
|
|
$osds = doGet('/api/osd');
|
|
$clusterConfig = doGet('/api/cluster_conf');
|
|
|
|
$mon_osd_nearfull_ratio = NULL;
|
|
|
|
logStr('Done');
|
|
logStr('Parsing..');
|
|
|
|
foreach ($clusterConfig as $configKey) {
|
|
|
|
if ($configKey->name === 'mon_osd_nearfull_ratio') {
|
|
$mon_osd_nearfull_ratio = $configKey->default; // Todo: Get the actual value
|
|
}
|
|
|
|
}
|
|
|
|
if ($mon_osd_nearfull_ratio === NULL) {
|
|
exitWithStatus('UNKNOWN', 'Unable to find config key "mon_osd_nearfull_ratio"');
|
|
}
|
|
|
|
$basePercentageEmpty = (100 - ($mon_osd_nearfull_ratio * 100));
|
|
|
|
$clusterCapacity = 0;
|
|
$clusterUsed = 0;
|
|
$nodeInfo = [];
|
|
|
|
foreach ($osds as $osd) {
|
|
|
|
$id = $osd->osd;
|
|
$stats = $osd->osd_stats;
|
|
|
|
$sizeInBytes = $stats->kb * 1024;
|
|
$usedSizeInBytes = $stats->kb_used * 1024;
|
|
|
|
$hostId = $osd->host->id;
|
|
$hostName = $osd->host->name;
|
|
|
|
if (!isset($nodeInfo[$hostId])) {
|
|
$nodeInfo[$hostId] = [
|
|
'name' => $hostName,
|
|
'capacity' => 0,
|
|
];
|
|
}
|
|
|
|
$nodeInfo[$hostId]['capacity'] += $sizeInBytes;
|
|
|
|
$clusterCapacity += $sizeInBytes;
|
|
$clusterUsed += $usedSizeInBytes;
|
|
|
|
}
|
|
|
|
$emptySpaceRequiredPercentage = $basePercentageEmpty;
|
|
|
|
usort($nodeInfo, function(array $infoA, $infoB) {
|
|
return $infoB['capacity'] <=> $infoA['capacity'];
|
|
});
|
|
|
|
foreach ($nodeInfo as $node) {
|
|
|
|
$nodePercentage = (100 * $node['capacity']) / $clusterCapacity;
|
|
logStr($node['name'] . ': ' . readableSize($node['capacity']) . ' (' . round($nodePercentage, 2) . '%)');
|
|
|
|
}
|
|
|
|
$i = 0;
|
|
foreach ($nodeInfo as $node) {
|
|
|
|
if ($i >= $allowedNodeFailures) {
|
|
break;
|
|
}
|
|
|
|
$nodePercentage = (100 * $node['capacity']) / $clusterCapacity;
|
|
|
|
$emptySpaceRequiredPercentage += $nodePercentage;
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
$maxUsagePercentage = 100 - $emptySpaceRequiredPercentage;
|
|
$usagePercentage = (100 / $clusterCapacity) * $clusterUsed;
|
|
|
|
$additionalInfo = '';
|
|
$severity = 'OK';
|
|
|
|
if ($usagePercentage > $maxUsagePercentage) {
|
|
|
|
$severity = 'CRITICAL';
|
|
|
|
$percentageRequired = ($usagePercentage - $maxUsagePercentage);
|
|
$bytesRequired = ($clusterCapacity / 100) * $percentageRequired;
|
|
|
|
$additionalInfo = ', at least an additional ' . readableSize($bytesRequired) . ' of capacity is required';
|
|
|
|
} elseif (($usagePercentage + $warningPercentageOffset) > $maxUsagePercentage) {
|
|
$severity = 'WARNING';
|
|
}
|
|
|
|
exitWithStatus($severity, round($usagePercentage, 2) . '% used out of allowed ' . round($maxUsagePercentage, 2) . '%' . $additionalInfo);
|