477 lines
17 KiB
PHP
477 lines
17 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/* ---------- Global constants ---------- */
|
||
define('API_KEY', 'deadbeef'); // API key used for the Remove button
|
||
|
||
/* ---------- Utility functions ---------- */
|
||
date_default_timezone_set('America/Los_Angeles');
|
||
/**
|
||
* Escape HTML
|
||
*/
|
||
function h(string $s): string
|
||
{
|
||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
/**
|
||
* Load simple key/value pairs from a YAML file.
|
||
* Lines starting with '#' are ignored.
|
||
* The function returns an associative array.
|
||
*/
|
||
function loadYaml(string $path): array
|
||
{
|
||
if (!file_exists($path)) {
|
||
return [];
|
||
}
|
||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
$data = [];
|
||
foreach ($lines as $line) {
|
||
$line = trim($line);
|
||
if ($line === '' || $line[0] === '#') {
|
||
continue;
|
||
}
|
||
$pos = strpos($line, ':');
|
||
if ($pos === false) {
|
||
continue;
|
||
}
|
||
$key = trim(substr($line, 0, $pos));
|
||
$value = trim(substr($line, $pos + 1));
|
||
$value = trim($value, "\"'"); // remove surrounding quotes
|
||
if ($value === '') {
|
||
$value = null;
|
||
}
|
||
$data[$key] = $value;
|
||
}
|
||
return $data;
|
||
}
|
||
/* ---------- Load settings ---------- */
|
||
$settingsPath = '/app/cosmostat_settings.yaml';
|
||
$settings = loadYaml($settingsPath);
|
||
|
||
/* ---------- Page mode handling ---------- */
|
||
$mode = $_GET['mode'] ?? 'cosmostat'; // default mode
|
||
$validModes = ['cosmostat', 'drive_health']; // extend as needed
|
||
if (!in_array($mode, $validModes, true)) {
|
||
$mode = 'cosmostat';
|
||
}
|
||
|
||
|
||
/* ---------- Process a POST “remove” request ---------- */
|
||
/* ---------- Helper: remove client details ---------- */
|
||
function removeClient($clientList): array
|
||
{
|
||
$bindIp = "172.25.1.18";
|
||
$port = "5001";
|
||
$path = "/storage_client_delete";
|
||
// Build the request payload.
|
||
$payload = [
|
||
'bind_ip' => $bindIp,
|
||
'port' => $port,
|
||
'path' => $path,
|
||
'API_KEY' => API_KEY,
|
||
'remove_hosts' => $clientList,
|
||
];
|
||
$jsonPayload = json_encode($payload);
|
||
if ($jsonPayload === false) {
|
||
// Encoding failed – return a clear error.
|
||
return [
|
||
'status' => 'error',
|
||
'code' => 0,
|
||
'message' => 'Failed to encode request payload: ' . json_last_error_msg(),
|
||
];
|
||
}
|
||
|
||
// Prepare the HTTP context for a POST request.
|
||
$headers = [
|
||
"Content-Type: application/json",
|
||
"Content-Length: " . strlen($jsonPayload),
|
||
"User-Agent: PHP/" . PHP_VERSION,
|
||
];
|
||
|
||
$ctxOptions = [
|
||
'http' => [
|
||
'method' => 'POST',
|
||
'header' => implode("\r\n", $headers),
|
||
'content' => $jsonPayload,
|
||
'timeout' => 5,
|
||
'ignore_errors' => true, // Still receive body for 4xx/5xx
|
||
]
|
||
];
|
||
|
||
$ctx = stream_context_create($ctxOptions);
|
||
|
||
|
||
$url = "http://172.25.1.18:5001/storage_client_delete";
|
||
$ctx = stream_context_create([
|
||
'http' => [
|
||
'timeout' => 5,
|
||
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
|
||
]
|
||
]);
|
||
$json = @file_get_contents($url, false, $ctx);
|
||
if ($json === false) {
|
||
return []; // caller will handle empty case
|
||
}
|
||
$data = json_decode($json, true);
|
||
if (!is_array($data)) {
|
||
return ['fail'];
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
$removedHostsMsg = '';
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$raw = file_get_contents('php://input');
|
||
$data = json_decode($raw, true);
|
||
if (json_last_error() === JSON_ERROR_NONE
|
||
&& isset($data['API_KEY'], $data['remove_hosts'])
|
||
&& $data['API_KEY'] === API_KEY
|
||
) {
|
||
/* ---- For demo purposes we just echo a message. In a real
|
||
system you would perform the removal logic here. ---- */
|
||
$removedHostsMsg = '<div class="card"><p>Removed hosts: '
|
||
. h(implode(', ', $data['remove_hosts']))
|
||
. '</p></div>';
|
||
echo "Time to remove ".$data['remove_hosts']."<br";
|
||
// removeClient($clientList);
|
||
}
|
||
}
|
||
|
||
/* ---------- API configuration per mode ---------- */
|
||
$apiConfig = [
|
||
'cosmostat' => ['bind' => '10.200.27.20', 'port' => '5000'],
|
||
'drive_health' => ['bind' => '172.25.1.18', 'port' => '5001'], // new API
|
||
];
|
||
|
||
/* ---------- Helper: fetch client details ---------- */
|
||
function fetchClientDetails(string $bindIp, string $port, string $path = '/client_details'): array
|
||
{
|
||
$url = "http://{$bindIp}:{$port}{$path}";
|
||
$ctx = stream_context_create([
|
||
'http' => [
|
||
'timeout' => 5,
|
||
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
|
||
]
|
||
]);
|
||
$json = @file_get_contents($url, false, $ctx);
|
||
if ($json === false) {
|
||
return []; // caller will handle empty case
|
||
}
|
||
$data = json_decode($json, true);
|
||
if (!is_array($data)) {
|
||
return ['fail'];
|
||
}
|
||
return $data;
|
||
}
|
||
/* ---------- Fetch client details ---------- */
|
||
$apiInfo = $apiConfig[$mode];
|
||
$clients = fetchClientDetails($apiInfo['bind'], $apiInfo['port']);
|
||
|
||
/* ---------- Ensure each client has a short_id ---------- */
|
||
foreach ($clients as &$client) {
|
||
if (!isset($client['short_id'])) {
|
||
$client['short_id'] = substr($client['uuid'] ?? '', 0, 8);
|
||
}
|
||
}
|
||
unset($client);
|
||
/* ---------- Determine selected hosts (Drive Health only) ---------- */
|
||
$selectedHosts = $_GET['hosts'] ?? [];
|
||
if ($mode === 'drive_health') {
|
||
if (isset($_GET['action'])) {
|
||
switch ($_GET['action']) {
|
||
case 'all':
|
||
$selectedHosts = array_column($clients, 'short_id');
|
||
break;
|
||
case 'none':
|
||
$selectedHosts = [];
|
||
break;
|
||
// 'apply' – nothing to do; $selectedHosts already contains the posted hosts
|
||
}
|
||
}
|
||
}
|
||
/* ---- Determine selected host ---- */
|
||
$selectedId = $_GET['host'] ?? '';
|
||
$selectedIdx = null;
|
||
foreach ($clients as $idx => $client) {
|
||
if (isset($client['short_id']) && $client['short_id'] === $selectedId) {
|
||
$selectedIdx = $idx;
|
||
break;
|
||
}
|
||
}
|
||
if ($selectedIdx === null) {
|
||
// Default to the first client (if any)
|
||
$selectedIdx = 0;
|
||
$selectedId = $clients[$selectedIdx]['short_id'] ?? '';
|
||
}
|
||
$client = $clients[$selectedIdx] ?? null;
|
||
$properties = $client['client_properties'][0] ?? [];
|
||
$systemProperties = $properties['system_properties'] ?? [];
|
||
$systemComponents = $properties['system_components'] ?? [];
|
||
$selectedHost = $clients[$selectedIdx]['hostname'] ?? 'Unknown';
|
||
|
||
/* ---- ---- */
|
||
/* ---- Sidebar Renderer ---- */
|
||
function renderSidebar(string $mode){
|
||
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx, $removedHostsMsg;
|
||
/* Show a removal‑success message if we just processed a POST */
|
||
if ($removedHostsMsg) {
|
||
echo $removedHostsMsg;
|
||
}
|
||
$modes = [
|
||
'cosmostat' => 'Cosmostat',
|
||
'drive_health' => 'Drive Health',
|
||
];
|
||
?>
|
||
<nav class="sidebar">
|
||
<form method="get" id="modeForm">
|
||
<label for="modeSelect">Mode:</label>
|
||
<select name="mode" id="modeSelect" onchange="this.form.submit()">
|
||
<?php foreach ($modes as $key => $label): ?>
|
||
<option value="<?= h($key) ?>" <?= $mode === $key ? 'selected' : '' ?>>
|
||
<?= h($label) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</form>
|
||
|
||
<?php if ($mode === 'drive_health'): ?>
|
||
<?php
|
||
if ( !is_array($clients) || empty($clients) ) {
|
||
echo '<p style="margin:1rem 0; font-style:italic;">'
|
||
. 'No hosts are available to manage at this time.'
|
||
. '</p>';
|
||
}
|
||
?>
|
||
<?php if (is_array($clients) && !empty($clients)): ?>
|
||
<h3>Hosts</h3>
|
||
<form method="get" id="driveHealthForm">
|
||
<input type="hidden" name="mode" value="drive_health">
|
||
<ul>
|
||
<?php foreach ($clients as $c): ?>
|
||
<?php $id = $c['short_id']; ?>
|
||
<li>
|
||
<label>
|
||
<input type="checkbox" name="hosts[]" value="<?= h($id) ?>"
|
||
<?= in_array($id, $selectedHosts, true) ? 'checked' : '' ?>>
|
||
<?= h($c['name']) ?>
|
||
</label>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul><p>
|
||
<button type="submit" name="action" value="apply">Apply</button><p>
|
||
<button type="submit" name="action" value="all">Select All</button><br>
|
||
<button type="submit" name="action" value="none">Select None</button><p>
|
||
</form>
|
||
|
||
<!-- Remove button (POST → same page) -->
|
||
<form id="removeHostsForm" method="post" action="">
|
||
<!-- API key is added by JS; we keep this form so the button can be inside it -->
|
||
<button type="button" id="removeButton">Remove Selected Hosts</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($mode == 'cosmostat'): ?>
|
||
<input type="hidden" name="host" value="<?= h($selectedId) ?>">
|
||
<h3>Endpoints</h3>
|
||
<ol id="endpointList"></ol>
|
||
<?php endif; ?>
|
||
<?php if ($mode == 'gali'): ?>
|
||
<h3>Shuttle Gali</h3>
|
||
<?php endif; ?>
|
||
</nav>
|
||
<?php
|
||
}
|
||
|
||
/* ---- Main Content Renderer ---- */
|
||
function renderMainContent(string $mode){
|
||
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx;
|
||
/* ---------- Handle empty / error ---------- */
|
||
if ($clients === ['fail']) {
|
||
die('<div class="card"><p style="color:red;">Could not retrieve data from the API for mode "' . h($mode) . '".</p></div>');
|
||
}
|
||
if ($mode === 'drive_health') {
|
||
// If nothing is selected, show a friendly message
|
||
if (empty($selectedHosts)) {
|
||
echo '<div class="card"><p>No hosts selected.</p></div>';
|
||
return;
|
||
}
|
||
echo '<div class="storage_client">';
|
||
foreach ($selectedHosts as $sid) {
|
||
// Find the client that matches this short_id
|
||
$c = null;
|
||
foreach ($clients as $cl) {
|
||
if ($cl['short_id'] === $sid) {
|
||
$c = $cl;
|
||
break;
|
||
}
|
||
}
|
||
if ($c === null) continue; // safety
|
||
$hostname = $c['name'] ?? 'Unknown';
|
||
echo '<div class="card">';
|
||
echo '<h2>Drive Health - ' . h($hostname) . '</h2>';
|
||
echo '<h3>IP: ' . h($c['ip']) . '<br>';
|
||
echo 'Timestamp: ' . date('F j, Y g:i a', (int) $c['timestamp']) . '</h3>';
|
||
if (isset($c['drives']) && is_array($c['drives']) && count($c['drives']) > 0) {
|
||
echo '<table id="host_metrics_table">';
|
||
echo '<thead><tr><th>Drive Letter</th><th>Model</th><th>Capacity</th><th>Power On Hours</th><th>Host Writes</th><th>Wear Level</th></tr></thead>';
|
||
echo '<tbody>';
|
||
foreach ($c['drives'] as $drive) {
|
||
echo '<tr>';
|
||
echo '<td>' . h($drive['drive_letter'] ?? '') . '</td>';
|
||
echo '<td>' . h($drive['model'] ?? '') . '</td>';
|
||
echo '<td>' . h($drive['capacity'] ?? '') . '</td>';
|
||
echo '<td>' . h($drive['power_on_hours'] ?? '') . '</td>';
|
||
echo '<td>' . h($drive['host_writes'] ?? '') . '</td>';
|
||
echo '<td>' . h($drive['wear_level'] ?? '') . '</td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody></table>';
|
||
} else {
|
||
echo '<p>No drive data available for this host.</p>';
|
||
}
|
||
echo '</div>';
|
||
}
|
||
echo '</div>';
|
||
return;
|
||
}
|
||
?>
|
||
<div class="main">
|
||
<?php if ($mode == 'cosmostat'): ?><!-- Header Card -->
|
||
<div class="card">
|
||
<h2>Matt-Cloud Cosmostat Dashboard</h2>
|
||
<p>This dashboard shows the local Matt-Cloud system stats.</p>
|
||
<div class="help-link" id="helpToggle">API</div>
|
||
</div> <!-- / Header Card -->
|
||
<!-- Hidden API Card -->
|
||
<div id="helpText" class="card">
|
||
<strong>Component Desriptor</strong>
|
||
<p>To view the component descriptor, you may <br>
|
||
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
|
||
This will return the entire JSON descriptor variable.<br>
|
||
The agent then reports back to the Cosmostat Server with all the data found in the descriptor.<br>
|
||
Full Source Code can be found at its <a target="_blank" rel="noopener noreferrer" href="https://gitea.matt-cloud.com/matt/cosmoserver">Gitea</a> page.
|
||
</div> <!-- / Header Card -->
|
||
<!-- summary card -->
|
||
<div class="card">
|
||
<?php if (!empty($systemProperties)): ?>
|
||
<h2>System Properties</h2>
|
||
<table><tr>
|
||
<td>
|
||
<ul class="system-list">
|
||
<?php foreach ($systemProperties as $prop): ?>
|
||
<li><?= h($prop['Property']) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</td>
|
||
<td>
|
||
<h2>Live System Metrics</h2>
|
||
<div id="host_metrics" class="column">Connecting...</div>
|
||
</td>
|
||
</tr></table>
|
||
<?php endif; ?><br>
|
||
<div class="componentDetail-link" id="componentDetailToggle">Toggle Component Details</div>
|
||
</div> <!--/summary card -->
|
||
|
||
<!-- hidden detail card -->
|
||
<div id="componentDetailText" class="card">
|
||
<?php if (!empty($systemComponents)): ?>
|
||
<h2>Components</h2>
|
||
<div class="components">
|
||
<?php foreach ($systemComponents as $comp): ?>
|
||
<div class="component">
|
||
<h3><?= h($comp['component_name']) ?></h3>
|
||
<ul class="info-list">
|
||
<?php foreach ($comp['info_strings'] as $info): ?>
|
||
<li><?= h($info) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div> <!--/hidden detail card -->
|
||
<?php endif; ?>
|
||
|
||
<?php if ($mode == 'gali'): ?>
|
||
<h3>Shuttle Gali</h3>
|
||
<?php endif; ?>
|
||
|
||
</div> <!-- /main -->
|
||
<?php
|
||
}
|
||
|
||
/* ---- Render server dashboard ---- */
|
||
?>
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Cosmostat - <?= h($selectedHost) ?></title>
|
||
<link rel="stylesheet" href="src/styles.css">
|
||
</head>
|
||
<body>
|
||
<div class="wrapper">
|
||
<!-- Sidebar -->
|
||
<?php renderSidebar($mode); ?>
|
||
<!-- Main content -->
|
||
<?php renderMainContent($mode); ?>
|
||
|
||
</div> <!-- /wrapper -->
|
||
<script src="socket.io/socket.io.js"></script>
|
||
<script src="src/system_metrics.js"></script>
|
||
<script>
|
||
/* Help / detail toggles */
|
||
document.getElementById('helpToggle')?.addEventListener('click', function () {
|
||
const help = document.getElementById('helpText');
|
||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||
});
|
||
document.getElementById('componentDetailToggle')?.addEventListener('click', function () {
|
||
const help = document.getElementById('componentDetailText');
|
||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||
});
|
||
|
||
/* --- Remove button logic --- */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const removeBtn = document.getElementById('removeButton');
|
||
if (!removeBtn) return; // not in drive_health mode
|
||
|
||
removeBtn.addEventListener('click', function () {
|
||
// Grab all checked hosts from the drive‑health form
|
||
const checked = document.querySelectorAll('input[name="hosts[]"]:checked');
|
||
const hosts = Array.from(checked).map(cb => cb.value);
|
||
|
||
if (hosts.length === 0) {
|
||
alert('No hosts selected.');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
API_KEY: '<?= API_KEY ?>',
|
||
remove_hosts: hosts
|
||
};
|
||
|
||
fetch('', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(payload)
|
||
})
|
||
.then(resp => {
|
||
if (!resp.ok) throw new Error('Network error');
|
||
return resp.text(); // we just reload on any response
|
||
})
|
||
.then(() => {
|
||
// reload the same index page
|
||
window.location.reload();
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
alert('Error removing hosts.');
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|