Add VIO FC Status monitoring page and enhance server inventory filtering

- Introduced `Storage/VIO.php` for monitoring VIO Fibre Channel status with client balancing checks and table sorting.
- Updated `/include/en.php` and `/include/fr.php` to include new translations for ServiceNow and VIO-related navigation elements.
- Improved SQL query and filtering logic in `/X/Inventory.php` to exclude VIO entries and add XenSam version data.
- Enhanced `/reports/heartbeat.php` with a new mechanism for handling server heartbeat status, allowing better granularity and readability.
This commit is contained in:
2026-09-14 10:32:15 +02:00
parent 1f1b7dcd22
commit c185d099d6
25 changed files with 6646 additions and 437 deletions

378
Storage/VIO.php Normal file
View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Infra Reports IT - VIO FC Status</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<script src="/js/jquery-3.6.1.min.js"></script>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/bootstrap-icons/bootstrap-icons.css">
<script src="/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/css/bootstrap-table.min.css">
<script src="/js/bootstrap-table.min.js"></script>
<script src="/js/bootstrap-table-fr-FR.min.js"></script>
<style>
.card-header a {
text-decoration: none;
color: white;
display: block;
width: 100%;
}
.card-header a:hover {
color: #f0f0f0;
}
</style>
</head>
<body class="bg-light text-dark">
<?php
include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Couple key = VIO name without its trailing number.
// e.g. "dun1vio1" and "dun1vio2" -> "dun1vio"
function vio_couple_key($vio) {
return preg_replace('/\d+$/', '', strtolower(trim($vio)));
}
// Parity of a physical fc port (fcname): "fcs0" -> even, "fcs1" -> odd, ...
function fcs_parity($fcs) {
if (preg_match('/(\d+)\s*$/', $fcs, $m)) {
return (intval($m[1]) % 2 === 0) ? 'even' : 'odd';
}
return 'unknown';
}
// Balancing rule for one client on a given couple.
// Returns null when balanced, otherwise a short problem label (FR).
// Expected: exactly 2 paths, on the 2 different VIOs, one even fcs + one odd fcs.
function evaluate_client_balance($paths) {
$nRows = count($paths);
$vios = array_unique(array_map(fn($p) => $p['vio'], $paths));
$evens = array_filter($paths, fn($p) => $p['parity'] === 'even');
$odds = array_filter($paths, fn($p) => $p['parity'] === 'odd');
$unknown = array_filter($paths, fn($p) => $p['parity'] === 'unknown');
if (count($unknown) > 0) {
return 'fcs illisible';
}
if ($nRows > 2) {
return $nRows . ' chemins (plus de 2)';
}
if (count($vios) < 2) {
return $nRows === 1 ? 'présent sur un seul VIO (chemin manquant)' : 'les 2 chemins sur le même VIO';
}
if (count($evens) !== 1 || count($odds) !== 1) {
return 'même parité sur les 2 VIO';
}
return null;
}
// Human-readable current distribution of a client's paths.
function format_client_paths($paths) {
$parts = [];
foreach ($paths as $p) {
$lbl = $p['parity'] === 'even' ? 'pair' : ($p['parity'] === 'odd' ? 'impair' : '?');
$parts[] = strtoupper($p['vio']) . ':' . $p['fcname'] . ' (' . $lbl . ')';
}
return implode(' | ', $parts);
}
// ---------------------------------------------------------------------------
// Data: FC status per VIO
// ---------------------------------------------------------------------------
$vioRows = Invoke_Infra("select * from X_fcstat_vio");
$groupedVios = [];
if (!empty($vioRows)) {
foreach ($vioRows as $row) {
$vioName = trim($row['vio'] ?? 'UNKNOWN');
$groupedVios[$vioName][] = $row;
}
}
// Group the VIOs into couples, and keep couples + members naturally sorted.
$couples = [];
foreach ($groupedVios as $vioName => $rows) {
$couples[vio_couple_key($vioName)][$vioName] = $rows;
}
ksort($couples, SORT_NATURAL | SORT_FLAG_CASE);
foreach ($couples as $ck => &$members) {
ksort($members, SORT_NATURAL | SORT_FLAG_CASE);
}
unset($members);
// ---------------------------------------------------------------------------
// Data: client mapping for the balancing check
// Error lines (inerr <> 0 or VIOErr <> 0) are excluded (shown on a dedicated page).
// ---------------------------------------------------------------------------
$cmdbRows = Invoke_Infra("select vio, clintname, fcname, vfcclientname, inerr, VIOErr from X_cmdb_VIO");
$clientPaths = []; // [coupleKey][clintname][] = ['vio','vfc','parity']
if (!empty($cmdbRows)) {
foreach ($cmdbRows as $r) {
if (intval($r['inerr'] ?? 0) !== 0 || intval($r['VIOErr'] ?? 0) !== 0) {
continue;
}
$vio = strtolower(trim($r['vio'] ?? ''));
$client = trim($r['clintname'] ?? '');
if ($vio === '' || $client === '') {
continue;
}
// Redundancy is driven by the PHYSICAL fc port on the VIO (fcname),
// not by vfcclientname (client-side virtual adapter, almost always fcs0/fcs1).
$fcname = strtolower(trim($r['fcname'] ?? ''));
$vfc = strtolower(trim($r['vfcclientname'] ?? ''));
$clientPaths[vio_couple_key($vio)][$client][] = [
'vio' => $vio,
'fcname' => $fcname,
'vfc' => $vfc,
'parity' => fcs_parity($fcname),
];
}
}
?>
<div class="container-fluid" id="content">
<div class="row flex-nowrap">
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark vh-100 position-sticky top-0" style="-ms-flex: 0 0 230px;flex: 0 0 230px;">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/navbar.html"; ?>
</div>
<div class="col py-3">
<h1>
<span class="badge text-bg-secondary font-weight-bold" style="width:100%;">
VIO FC Status
</span>
</h1>
<div class="container-fluid mt-4">
<div id="accordion">
<?php if (!empty($couples)): ?>
<?php $vioIndex = 0; ?>
<?php foreach ($couples as $coupleKey => $coupleVios): ?>
<?php /* -------- VIO cards of the couple -------- */ ?>
<?php foreach ($coupleVios as $vioName => $rows): ?>
<?php
$vioIndex++;
$collapseId = "collapse_vio_" . $vioIndex;
usort($rows, function ($a, $b) {
$cmpFcs = strnatcasecmp($a['fcs'] ?? '', $b['fcs'] ?? '');
if ($cmpFcs !== 0) {
return $cmpFcs;
}
$cmpFw = strnatcasecmp($a['fw'] ?? '', $b['fw'] ?? '');
if ($cmpFw !== 0) {
return $cmpFw;
}
return strnatcasecmp($a['atype'] ?? '', $b['atype'] ?? '');
});
?>
<?php
$vioHasAlert = false;
foreach ($rows as $checkRow) {
$typeIsDown = strtolower(trim($checkRow['atype'] ?? '')) === 'down';
$tsIsOld = false;
if (!empty($checkRow['ts'])) {
try {
$tsDate = new DateTime($checkRow['ts']);
$now = new DateTime();
$diffSeconds = $now->getTimestamp() - $tsDate->getTimestamp();
$tsIsOld = $diffSeconds > 86400;
} catch (Exception $e) {
$tsIsOld = false;
}
}
if ($typeIsDown || $tsIsOld) {
$vioHasAlert = true;
break;
}
}
$cardColor = $vioHasAlert ? 'red' : 'green';
?>
<div class="card" style="background-color:<?php echo $cardColor; ?>;">
<h4 class="card-header">
<a class="btn text-white fs-6 w-100 text-start" data-bs-toggle="collapse" href="#<?php echo $collapseId; ?>">
<b><?php echo htmlspecialchars(strtoupper($vioName)); ?></b>
</a>
</h4>
</div>
<div id="<?php echo $collapseId; ?>" class="collapse" data-bs-parent="#accordion">
<div class="card-body bg-white border p-0">
<div class="table-responsive">
<table class="table table-bordered table-hover mb-0 text-center" data-toggle="table">
<thead class="table-light">
<tr>
<th data-sortable="true">FCS</th>
<th data-sortable="true">FW</th>
<th data-sortable="true">Speed</th>
<th data-sortable="true">Type</th>
<th data-sortable="true">Uports</th>
<th data-sortable="true">Last</th>
<th data-sortable="true">TFrames</th>
<th data-sortable="true">RFrames</th>
<th data-sortable="true">Fail</th>
<th data-sortable="true">LossC</th>
<th data-sortable="true">LossS</th>
<th data-sortable="true">InvWord</th>
<th data-sortable="true">InvCRC</th>
<th data-sortable="true">TS</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$typeIsDown = strtolower(trim($row['atype'] ?? '')) == 'down';
$tsIsOld = false;
if (!empty($row['ts'])) {
try {
$tsDate = new DateTime($row['ts']);
$now = new DateTime();
$diffSeconds = $now->getTimestamp() - $tsDate->getTimestamp();
$tsIsOld = $diffSeconds > 86400;
} catch (Exception $e) {
$tsIsOld = false;
}
}
$rowClass = ($typeIsDown || $tsIsOld) ? 'table-danger fw-bold' : '';
$typeClass = $typeIsDown ? 'bg-danger text-white fw-bold' : '';
$tsClass = $tsIsOld ? 'bg-danger text-white fw-bold' : '';
?>
<tr class="<?php echo $rowClass; ?>">
<td><?php echo htmlspecialchars($row['fcs'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['fw'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['speed'] ?? ''); ?></td>
<td class="<?php echo $typeClass; ?>"><?php echo htmlspecialchars($row['atype'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['uports'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['last'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['tframes'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['rframes'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['fail'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['lossc'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['losss'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['invword'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['invcrc'] ?? ''); ?></td>
<td class="<?php echo $tsClass; ?>" style="white-space: nowrap;"><?php echo htmlspecialchars($row['ts'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endforeach; ?>
<?php /* -------- Balancing block for this couple -------- */ ?>
<?php
$coupleClients = $clientPaths[$coupleKey] ?? [];
$flagged = [];
$okCount = 0;
foreach ($coupleClients as $clientName => $paths) {
$problem = evaluate_client_balance($paths);
if ($problem === null) {
$okCount++;
} else {
$flagged[$clientName] = ['problem' => $problem, 'paths' => $paths];
}
}
ksort($flagged, SORT_NATURAL | SORT_FLAG_CASE);
$hasData = !empty($coupleClients);
$balOk = $hasData && empty($flagged);
$balColor = !$hasData ? '#6c757d' : ($balOk ? 'green' : 'red');
?>
<div class="card mb-2" style="border-color:<?php echo $balColor; ?>;">
<div class="card-header text-white text-center fw-bold" style="background-color:<?php echo $balColor; ?>;">
<i class="bi bi-diagram-3"></i>
Équilibrage <?php echo htmlspecialchars(strtoupper($coupleKey)); ?>
<?php if (!$hasData): ?>
— aucune donnée client
<?php elseif ($balOk): ?>
— OK (<?php echo $okCount; ?> clients vérifiés)
<?php else: ?>
— <?php echo count($flagged); ?> client(s) à corriger (<?php echo $okCount; ?> OK)
<?php endif; ?>
</div>
<?php if ($hasData && !$balOk): ?>
<div class="card-body bg-white p-0">
<div class="table-responsive">
<table class="table table-bordered table-hover mb-0" data-toggle="table">
<thead class="table-light">
<tr>
<th data-sortable="true">Client</th>
<th data-sortable="true">Répartition actuelle</th>
<th data-sortable="true">Problème</th>
</tr>
</thead>
<tbody>
<?php foreach ($flagged as $clientName => $info): ?>
<tr class="table-danger">
<td class="fw-bold"><?php echo htmlspecialchars($clientName); ?></td>
<td><?php echo htmlspecialchars(format_client_paths($info['paths'])); ?></td>
<td><?php echo htmlspecialchars($info['problem']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<hr>
<?php endforeach; ?>
<?php else: ?>
<div class="alert alert-warning">
No VIO FC data available.
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="/js/switch.js"></script>
</html>