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

View File

@@ -0,0 +1,450 @@
<!DOCTYPE html>
<html lang="fr">
<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>Web Infra Reports OT</title>
<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">
<link rel="stylesheet" href="/css/preloader.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>
<script src="/js/tableExport.min.js"></script>
<script src="/js/bootstrap-table-export.min.js"></script>
<script src="/js/libs/js-xlsx/xlsx.core.min.js"></script>
<style>
html, body {
height: 100%;
overflow: hidden;
}
.main-row {
height: 100vh;
}
.left-menu {
flex: 0 0 230px;
height: 100vh;
overflow-y: hidden;
}
.right-content {
height: 100vh;
overflow-y: auto;
padding-top: 0 !important;
}
.sticky-dashboard-header {
position: sticky;
top: 0;
z-index: 1000;
padding-top: 1rem;
padding-bottom: .75rem;
}
.kpi-card { cursor: pointer; transition: transform .1s ease-in-out; }
.kpi-card:hover { transform: translateY(-2px); }
code.sig { font-size: .85em; }
.table td { vertical-align: middle; }
/* Chevron rotation for the collapsible unreachable list */
.collapse-toggle .bi-chevron-down { transition: transform .15s ease-in-out; }
.collapse-toggle[aria-expanded="true"] .bi-chevron-down { transform: rotate(180deg); }
</style>
</head>
<body>
<?php include $_SERVER['DOCUMENT_ROOT']."/include/all.php" ; ?>
<div class="container-fluid">
<div class="row flex-nowrap main-row">
<div class="col-auto px-sm-2 px-0 bg-dark left-menu">
<?php include $_SERVER['DOCUMENT_ROOT']."/navbar.html" ; ?>
</div>
<div class="col right-content">
<?php
// -----------------------------------------------------------
// Read the latest run from the compliance results table
//
// Cluster conformity is now keyed on the BUILD (BuildDrift):
// two nodes sharing the same build but a different UBR/KB are
// OK at cluster level. The UBR gap is kept as a severity /
// priority indicator. Hardware facts (BIOS + model) are read
// for display and light hardware-drift flagging.
// -----------------------------------------------------------
$conn = DB_INFRA();
$sql = "SELECT CONVERT(VARCHAR(36), RunId) AS RunId, ClusterName, NodeName, OsName,
MsPatchLevel, MsBuild, MsUbr, UbrGap, LastHotfix,
CONVERT(VARCHAR(10), LastHotfixOn, 120) AS LastHotfixOn,
SppSignature,
BiosVersion, CONVERT(VARCHAR(10), BiosDate, 120) AS BiosDate,
ServerModel, Manufacturer,
Reachable, MsDrift, BuildDrift, SppDrift, ErrorMessage,
CONVERT(VARCHAR(16), RunDate, 120) AS RunDate
FROM dbo.ClusterComplianceResults
WHERE RunId = (SELECT TOP 1 RunId FROM dbo.ClusterComplianceResults ORDER BY RunDate DESC)
AND (
Reachable = 0
OR ClusterName IN (
SELECT ClusterName
FROM dbo.ClusterComplianceResults
WHERE RunId = (SELECT TOP 1 RunId FROM dbo.ClusterComplianceResults ORDER BY RunDate DESC)
AND Reachable = 1
GROUP BY ClusterName
HAVING COUNT(DISTINCT NodeName) > 1
)
)
ORDER BY ClusterName, NodeName";
$rs = odbc_exec($conn, $sql);
$runId = "";
$runDate = "-";
$clusters = array();
if ($rs) {
while ($row = odbc_fetch_array($rs)) {
if ($runId === "") { $runId = $row['RunId']; $runDate = $row['RunDate']; }
$c = $row['ClusterName'];
if (!isset($clusters[$c])) {
$clusters[$c] = array(
'name'=>$c, 'nodes'=>array(),
'build'=>false, 'ms'=>false, 'spp'=>false,
'unreach'=>false, 'ubrgap'=>0
);
}
$clusters[$c]['nodes'][] = $row;
// Conformity fail = build drift. MS (Build.UBR) kept as info only.
if ($row['BuildDrift'] == 1) { $clusters[$c]['build'] = true; }
if ($row['MsDrift'] == 1) { $clusters[$c]['ms'] = true; }
if ($row['SppDrift'] == 1) { $clusters[$c]['spp'] = true; }
if ($row['Reachable'] == 0) { $clusters[$c]['unreach'] = true; }
// UbrGap is stored identically on every node row of the run.
$clusters[$c]['ubrgap'] = max($clusters[$c]['ubrgap'], (int)$row['UbrGap']);
}
}
// Classify each cluster: DRIFT > UNREACHABLE > OK
// DRIFT is now build-drift OR SPP-drift (NOT a pure UBR/KB gap).
$ok = array(); $drift = array(); $unreach = array(); $ubrgap_list = array();
foreach ($clusters as $name => $cl) {
if ($cl['build'] || $cl['spp']) {
$clusters[$name]['status'] = 'DRIFT';
if (count($cl['nodes']) > 1) {
$drift[] = $name;
}
} elseif ($cl['unreach']) {
$clusters[$name]['status'] = 'UNREACHABLE'; $unreach[] = $name;
} else {
if (count($cl['nodes']) > 1){
// Build + SPP aligned and fully reachable. A non-zero
// UBR gap means "conform at cluster level, but KB behind"
// -> isolated in its own priority list, out of the OK set.
if ((int)$clusters[$name]['ubrgap'] > 0) {
$clusters[$name]['status'] = 'UBR_GAP';
$ubrgap_list[] = $name;
} else {
$clusters[$name]['status'] = 'OK';
$ok[] = $name;
}
} else {
$clusters[$name]['status'] = 'OK';
}
}
}
$count_ok = count($ok);
$count_drift = count($drift);
$count_unreach = count($unreach);
$count_ubrgap = count($ubrgap_list);
// Biggest gaps first, so the priority list is correct regardless
// of any client-side sorting applied afterwards.
usort($ubrgap_list, function($a, $b) use ($clusters) {
return (int)$clusters[$b]['ubrgap'] <=> (int)$clusters[$a]['ubrgap'];
});
// Per-cluster summary (distinct MS levels, builds, SPP signatures,
// hardware facts, unreachable nodes, errors).
function cluster_summary($cl) {
$ms = array(); $builds = array(); $spp = array();
$models = array(); $bios = array(); $biosdates = array();
$unodes = array(); $errs = array();
foreach ($cl['nodes'] as $n) {
if ($n['Reachable'] == 1) {
if ($n['MsPatchLevel'] !== '' && $n['MsPatchLevel'] !== null) { $ms[] = $n['MsPatchLevel']; }
if ($n['MsBuild'] !== '' && $n['MsBuild'] !== null) { $builds[] = $n['MsBuild']; }
if ($n['SppSignature'] !== '' && $n['SppSignature'] !== null) { $spp[] = $n['SppSignature']; }
if ($n['ServerModel'] !== '' && $n['ServerModel'] !== null) { $models[] = $n['ServerModel']; }
if ($n['BiosVersion'] !== '' && $n['BiosVersion'] !== null) { $bios[] = $n['BiosVersion']; }
if ($n['BiosDate'] !== '' && $n['BiosDate'] !== null) { $biosdates[] = $n['BiosDate']; }
} else {
$unodes[] = $n['NodeName'];
if (!empty($n['ErrorMessage'])) { $errs[] = $n['ErrorMessage']; }
}
}
$bios_u = array_values(array_unique($bios));
return array(
'ms' => array_values(array_unique($ms)),
'builds' => array_values(array_unique($builds)),
'spp' => array_values(array_unique($spp)),
'models' => array_values(array_unique($models)),
'bios' => $bios_u,
'biosdates' => array_values(array_unique($biosdates)),
'bios_drift' => (count($bios_u) > 1),
'ubrgap' => (int)$cl['ubrgap'],
'unodes' => $unodes,
'errs' => array_values(array_unique($errs)),
'nodecount' => count($cl['nodes'])
);
}
// Small helper: a coloured badge for the UBR gap value.
function ubrgap_badge($gap) {
$gap = (int)$gap;
if ($gap <= 0) { return "<span class='badge text-bg-light text-muted'>0</span>"; }
$cls = ($gap >= 100) ? 'text-bg-danger' : 'text-bg-info';
return "<span class='badge $cls'>".$gap."</span>";
}
?>
<div class="sticky-dashboard-header bg-light">
<h1>
<span class="badge text-bg-secondary" style="width:100%;">
Conformité des Clusters &mdash; Build MS / SPP HP / Matériel
<small><small>(<?php echo htmlspecialchars($runDate); ?>)</small></small>
</span>
</h1>
<!-- ================= KPI cards ================= -->
<div class="row my-3">
<div class="col-md-3">
<a href="#sec_ok" class="text-decoration-none">
<div class="card text-bg-success shadow-sm kpi-card">
<div class="card-body py-2 d-flex justify-content-between align-items-center">
<div><h6 class="card-title mb-0"><i class="bi bi-check-circle me-1"></i>Clusters OK</h6></div>
<h2 class="fw-bold mb-0"><?php echo $count_ok; ?></h2>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="#sec_drift" class="text-decoration-none">
<div class="card text-bg-warning shadow-sm kpi-card">
<div class="card-body py-2 d-flex justify-content-between align-items-center">
<div><h6 class="card-title mb-0"><i class="bi bi-exclamation-triangle me-1"></i>Clusters en drift</h6></div>
<h2 class="fw-bold mb-0"><?php echo $count_drift; ?></h2>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="#sec_ubrgap" class="text-decoration-none">
<div class="card text-bg-info shadow-sm kpi-card">
<div class="card-body py-2 d-flex justify-content-between align-items-center">
<div><h6 class="card-title mb-0"><i class="bi bi-arrow-left-right me-1"></i>&Eacute;cart UBR (m&ecirc;me build)</h6></div>
<h2 class="fw-bold mb-0"><?php echo $count_ubrgap; ?></h2>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="#sec_unreach" class="text-decoration-none">
<div class="card text-bg-danger shadow-sm kpi-card">
<div class="card-body py-2 d-flex justify-content-between align-items-center">
<div><h6 class="card-title mb-0"><i class="bi bi-plug me-1"></i>Clusters injoignables</h6></div>
<h2 class="fw-bold mb-0"><?php echo $count_unreach; ?></h2>
</div>
</div>
</a>
</div>
</div>
</div>
<!-- ================= DRIFT table ================= -->
<h4 id="sec_drift" class="mt-4"><i class="bi bi-exclamation-triangle text-warning me-1"></i>Clusters en drift</h4>
<table class="table table-bordered table-hover table-sm" id="t_drift"
data-toggle="table" data-search="true" data-show-columns="true"
data-export-types="['xlsx','csv','json']" data-show-export="true"
data-sortable="true" style="width:99%;">
<thead>
<tr>
<th data-field="cluster" data-sortable="true">Cluster</th>
<th data-field="ecart" data-sortable="false">&Eacute;cart</th>
<th data-field="noeuds" data-sortable="true">N&oelig;uds</th>
<th data-field="build" data-sortable="false">Build(s)</th>
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
<th data-field="ms" data-sortable="false">Niveau(x) MS</th>
<th data-field="modele" data-sortable="false">Mod&egrave;le(s)</th>
<th data-field="bios" data-sortable="false">BIOS</th>
<th data-field="biosdate" data-sortable="false">Date BIOS</th>
<th data-field="spp" data-sortable="false">Signature(s) SPP</th>
<th data-field="detail" data-sortable="false">D&eacute;tail</th>
</tr>
</thead>
<tbody>
<?php foreach ($drift as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
<tr>
<td><?php echo htmlspecialchars($name); ?></td>
<td>
<?php if ($cl['build']) { echo "<span class='badge text-bg-danger me-1'>BUILD</span>"; } ?>
<?php if ($cl['spp']) { echo "<span class='badge text-bg-warning text-dark me-1'>SPP</span>"; } ?>
<?php if ($s['bios_drift']) { echo "<span class='badge text-bg-secondary'>BIOS</span>"; } ?>
</td>
<td><?php echo $s['nodecount']; ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['biosdates'])); ?></td>
<td><code class="sig"><?php echo htmlspecialchars(implode(' / ', $s['spp'])); ?></code></td>
<td>
<a class="btn btn-sm btn-outline-primary"
href="cluster_compliance_detail.php?cluster=<?php echo urlencode($name); ?>&run=<?php echo urlencode($runId); ?>">
<i class="bi bi-search"></i> D&eacute;tail
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ================= UBR GAP table (same build, KB behind) ================= -->
<h4 id="sec_ubrgap" class="mt-4"><i class="bi bi-arrow-left-right text-info me-1"></i>&Eacute;cart UBR &mdash; m&ecirc;me build, KB &agrave; rattraper</h4>
<p class="text-muted small mb-2">
Ces clusters sont conformes au niveau cluster (build align&eacute;), mais un ou plusieurs n&oelig;uds
sont en retard de correctif. Tri&eacute;s par &eacute;cart d&eacute;croissant &mdash; les plus gros &agrave; corriger en priorit&eacute;.
</p>
<table class="table table-bordered table-hover table-sm" id="t_ubrgap"
data-toggle="table" data-search="true" data-show-columns="true"
data-export-types="['xlsx','csv','json']" data-show-export="true"
data-sortable="true" style="width:99%;">
<thead>
<tr>
<th data-field="cluster" data-sortable="true">Cluster</th>
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
<th data-field="noeuds" data-sortable="true">N&oelig;uds</th>
<th data-field="build" data-sortable="true">Build</th>
<th data-field="ms" data-sortable="false">Niveau(x) MS</th>
<th data-field="modele" data-sortable="false">Mod&egrave;le</th>
<th data-field="bios" data-sortable="false">BIOS</th>
<th data-field="detail" data-sortable="false">D&eacute;tail</th>
</tr>
</thead>
<tbody>
<?php foreach ($ubrgap_list as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
<tr>
<td>
<?php echo htmlspecialchars($name); ?>
<?php if ($s['bios_drift']) { echo " <span class='badge text-bg-secondary'>BIOS</span>"; } ?>
</td>
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
<td><?php echo $s['nodecount']; ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
<td>
<a class="btn btn-sm btn-outline-primary"
href="cluster_compliance_detail.php?cluster=<?php echo urlencode($name); ?>&run=<?php echo urlencode($runId); ?>">
<i class="bi bi-search"></i> D&eacute;tail
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ================= OK table ================= -->
<h4 id="sec_ok" class="mt-4"><i class="bi bi-check-circle text-success me-1"></i>Clusters OK</h4>
<table class="table table-bordered table-hover table-sm" id="t_ok"
data-toggle="table" data-search="true" data-show-columns="true"
data-export-types="['xlsx','csv','json']" data-show-export="true"
data-sortable="true" style="width:99%;">
<thead>
<tr>
<th data-field="cluster" data-sortable="true">Cluster</th>
<th data-field="noeuds" data-sortable="true">N&oelig;uds</th>
<th data-field="build" data-sortable="true">Build</th>
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
<th data-field="ms" data-sortable="true">Niveau(x) MS</th>
<th data-field="modele" data-sortable="false">Mod&egrave;le</th>
<th data-field="bios" data-sortable="false">BIOS</th>
<th data-field="spp" data-sortable="false">Signature SPP</th>
</tr>
</thead>
<tbody>
<?php foreach ($ok as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
<tr>
<td>
<?php echo htmlspecialchars($name); ?>
<?php if ($s['bios_drift']) { echo " <span class='badge text-bg-secondary'>BIOS</span>"; } ?>
</td>
<td><?php echo $s['nodecount']; ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
<td><code class="sig"><?php echo htmlspecialchars(implode(', ', $s['spp'])); ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ================= UNREACHABLE (collapsed list, at the end) ================= -->
<h4 id="sec_unreach" class="mt-4 mb-2">
<a class="collapse-toggle text-decoration-none text-reset" data-bs-toggle="collapse"
href="#collapseUnreach" role="button" aria-expanded="false" aria-controls="collapseUnreach">
<i class="bi bi-plug text-danger me-1"></i>Clusters injoignables
<span class="badge text-bg-danger align-middle"><?php echo $count_unreach; ?></span>
<i class="bi bi-chevron-down small ms-1"></i>
</a>
</h4>
<div class="collapse mb-4" id="collapseUnreach">
<?php if ($count_unreach == 0): ?>
<p class="text-muted small mb-0">Aucun cluster injoignable sur ce run.</p>
<?php else: ?>
<ul class="list-group">
<?php foreach ($unreach as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
<li class="list-group-item">
<i class="bi bi-plug text-danger me-1"></i><strong><?php echo htmlspecialchars($name); ?></strong>
<?php if (!empty($s['unodes'])): ?>
<span class="text-muted">&mdash; <?php echo htmlspecialchars(implode(', ', $s['unodes'])); ?></span>
<?php endif; ?>
<?php if (!empty($s['errs'])): ?>
<div class="small text-muted mt-1"><?php echo htmlspecialchars(implode(' | ', $s['errs'])); ?></div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script src="/js/switch.js"></script>
<script>
// Clicking the "Clusters injoignables" KPI card scrolls to the section
// and expands the collapsed list.
document.addEventListener('DOMContentLoaded', function () {
var card = document.querySelector('a[href="#sec_unreach"]');
var box = document.getElementById('collapseUnreach');
if (card && box) {
card.addEventListener('click', function () {
bootstrap.Collapse.getOrCreateInstance(box, { toggle: false }).show();
});
}
});
</script>
</body>
</html>