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:
410
reports/Backups.php
Normal file
410
reports/Backups.php
Normal file
@@ -0,0 +1,410 @@
|
||||
<!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 IT</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>
|
||||
<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>
|
||||
/* ---- Theme --------------------------------------------------------
|
||||
switch.js only swaps -light/-dark classes and tags tables with
|
||||
.table-dark. The accordion paints its own background, so we make it
|
||||
transparent and let it inherit whatever the body is wearing. */
|
||||
.accordion,
|
||||
.accordion-item,
|
||||
.accordion-button,
|
||||
.accordion-button:not(.collapsed),
|
||||
.accordion-body { background-color: transparent; color: inherit; }
|
||||
.accordion-item { border-color: rgba(128, 128, 128, .35); }
|
||||
.accordion-button:not(.collapsed) { box-shadow: none; background-color: rgba(128, 128, 128, .12); }
|
||||
.accordion-button:focus{ box-shadow: none; border-color: rgba(128, 128, 128, .35); }
|
||||
.accordion-button::after { filter: grayscale(1) opacity(.75); }
|
||||
body.bg-dark .accordion-button::after { filter: invert(1) grayscale(1) opacity(.85); }
|
||||
|
||||
/* Sticky needs a non-clipping, non-scrolling ancestor chain. bootstrap-table
|
||||
wraps the table in .fixed-table-body (overflow:auto), which would capture
|
||||
the sticky thead and push it down inside the table instead of the page. */
|
||||
.accordion, .accordion-item { overflow: visible; }
|
||||
.accordion-body,
|
||||
.accordion-body .fixed-table-container,
|
||||
.accordion-body .fixed-table-body,
|
||||
.accordion-body .fixed-table-toolbar { overflow: visible !important; }
|
||||
.accordion-body .fixed-table-container { border: 0; }
|
||||
|
||||
/* ---- Sticky block header + sticky table head ---------------------- */
|
||||
.accordion-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.report-table thead th {
|
||||
position: sticky;
|
||||
top: var(--acc-header-h, 56px);
|
||||
z-index: 2;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
body.bg-dark .accordion-header,
|
||||
body.bg-dark .report-table thead th { background-color: #212529; }
|
||||
|
||||
.block-count { font-size: 1.5rem;min-width: 3.5em; }
|
||||
.accordion-button .block-title { font-size: 2rem; font-weight: 600; letter-spacing: .02em; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light text-dark">
|
||||
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
|
||||
|
||||
<?php
|
||||
/* ============================================================================
|
||||
Unified backup reporting : Veeam (target) + NetBackup (legacy)
|
||||
During the migration a VM can still exist in both SQL tables.
|
||||
It is displayed only on the side holding the most recent backup.
|
||||
========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------- helpers */
|
||||
|
||||
// Whole days between a date and today. Returns null when the date is unusable.
|
||||
function days_since($dateStr) {
|
||||
if (empty($dateStr)) return null;
|
||||
$ts = strtotime($dateStr);
|
||||
if ($ts === false || $ts < strtotime('1990-01-01')) return null;
|
||||
return (int) floor((strtotime(date('Y-m-d')) - strtotime(date('Y-m-d', $ts))) / 86400);
|
||||
}
|
||||
|
||||
// Backups do not run on Sunday night, so Sunday and Monday get a wider window.
|
||||
function backup_tolerance_days() {
|
||||
$dow = (int) date('w'); // 0 = Sunday, 1 = Monday
|
||||
return ($dow === 0 || $dow === 1) ? 4 : 3;
|
||||
}
|
||||
|
||||
function fmt_datetime($value) {
|
||||
$ts = strtotime((string) $value);
|
||||
return ($ts && $ts > strtotime('1990-01-01')) ? date('Y-m-d H:i', $ts) : '-';
|
||||
}
|
||||
|
||||
function fmt_lastgood($dateStr) {
|
||||
$days = days_since($dateStr);
|
||||
if ($days === null || $days > 365) return 'NEVER';
|
||||
return date('Y-m-d H:i', strtotime($dateStr)) . " ({$days}J)";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- active inventory */
|
||||
|
||||
$activeRows = Invoke_Infra("SELECT name FROM cmdb_vms
|
||||
WHERE CAST(lastinventory AS DATETIME) > DATEADD(DAY, -2, GETDATE())");
|
||||
$active = [];
|
||||
foreach ($activeRows as $r) { $active[strtoupper(trim($r['name']))] = true; }
|
||||
|
||||
/* -------------------------------------------------------------- 1. Veeam */
|
||||
|
||||
$veeam = [];
|
||||
$rows = Invoke_Infra("SELECT * FROM Veeam_Reporting");
|
||||
foreach ($rows as $row) {
|
||||
$key = strtoupper(trim($row['ClientName']));
|
||||
if (!isset($active[$key])) continue;
|
||||
|
||||
$ageGood = days_since($row['LastSuccess']);
|
||||
$status = trim((string) $row['LastStatus']);
|
||||
|
||||
if ($status === 'Failed' || $status === 'Unknown' || $status === '') {
|
||||
$cat = 'error'; $cls = 'table-danger';
|
||||
} elseif ($status === 'Warning') {
|
||||
$cat = 'warning'; $cls = 'table-warning';
|
||||
} elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) {
|
||||
$cat = 'outdated'; $cls = 'table-info';
|
||||
} else {
|
||||
$cat = 'ok'; $cls = 'table-success';
|
||||
}
|
||||
|
||||
$veeam[$key] = [
|
||||
'cat' => $cat,
|
||||
'row_class' => $cls,
|
||||
'ts' => strtotime((string) $row['LastRun']) ?: 0,
|
||||
'vm' => $row['ClientName'],
|
||||
'last_run' => fmt_datetime($row['LastRun']),
|
||||
'status' => $status,
|
||||
'last_good' => fmt_lastgood($row['LastSuccess']),
|
||||
'size' => $row['SizeGB'],
|
||||
'extra' => $row['Duration'],
|
||||
'policy' => $row['PolicyName'],
|
||||
'message' => $row['ErrorMessage'],
|
||||
];
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- 2. NetBackup */
|
||||
|
||||
$netbackup = [];
|
||||
$rows = Invoke_Infra("SELECT * FROM VMs_Backup
|
||||
WHERE (Owner LIKE 'DUN-VMH%' OR Owner LIKE 'MDK-VMH%')
|
||||
AND name NOT LIKE 'WS%'
|
||||
AND owner NOT LIKE '%WKG%'
|
||||
AND owner NOT LIKE '%VMH-WM%'
|
||||
ORDER BY name");
|
||||
foreach ($rows as $row) {
|
||||
$key = strtoupper(trim($row['Name']));
|
||||
$excluded = trim((string) $row['Exclusion']) !== '';
|
||||
|
||||
// Excluded VMs are listed even when no longer inventoried, like on the old page.
|
||||
if (!$excluded && !isset($active[$key])) continue;
|
||||
|
||||
$result = trim((string) $row['LastResult']);
|
||||
$ageGood = days_since($row['LastKnownGood']);
|
||||
|
||||
if ($excluded) {
|
||||
$cat = 'excluded'; $cls = 'table-secondary';
|
||||
} elseif ($result !== 'OK') {
|
||||
// A failed job with a still fresh good copy is only a warning.
|
||||
if ($ageGood === null || $ageGood >= backup_tolerance_days()) {
|
||||
$cat = 'error'; $cls = 'table-danger';
|
||||
} else {
|
||||
$cat = 'warning'; $cls = 'table-warning';
|
||||
}
|
||||
} elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) {
|
||||
$cat = 'outdated'; $cls = 'table-info';
|
||||
} else {
|
||||
$cat = 'ok'; $cls = 'table-success';
|
||||
}
|
||||
|
||||
$netbackup[$key] = [
|
||||
'cat' => $cat,
|
||||
'row_class' => $cls,
|
||||
'excluded' => $excluded,
|
||||
'ts' => strtotime(trim($row['LastBackup'] . ' ' . $row['TimeStamp'])) ?: 0,
|
||||
'vm' => $row['Name'],
|
||||
'last_run' => $excluded ? 'Tag NoBackup' : fmt_datetime($row['LastBackup'] . ' ' . $row['TimeStamp']),
|
||||
'status' => $excluded ? 'Tag NoBackup' : ($result !== '' ? $result : 'Unknown'),
|
||||
'last_good' => $excluded ? 'Tag NoBackup' : fmt_lastgood($row['LastKnownGood']),
|
||||
'size' => $excluded ? '' : $row['LastSize'],
|
||||
'extra' => $row['Owner'],
|
||||
'policy' => $row['Policy'],
|
||||
'message' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------- 3. Migration de-duplication */
|
||||
// A VM present on both sides is kept where the most recent backup lives.
|
||||
|
||||
$migrated = 0;
|
||||
foreach (array_keys($netbackup) as $key) {
|
||||
if (!isset($veeam[$key])) continue;
|
||||
|
||||
if ($netbackup[$key]['excluded'] || $veeam[$key]['ts'] >= $netbackup[$key]['ts']) {
|
||||
unset($netbackup[$key]); // Veeam has taken over
|
||||
$migrated++;
|
||||
} else {
|
||||
unset($veeam[$key]); // NetBackup is still the live copy
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- 4. Bucketing */
|
||||
|
||||
$buckets = [
|
||||
'veeam' => ['error' => [], 'warning' => [], 'outdated' => [], 'ok' => []],
|
||||
'nb' => ['error' => [], 'warning' => [], 'outdated' => [], 'ok' => [], 'excluded' => []],
|
||||
];
|
||||
foreach ($veeam as $e) { $buckets['veeam'][$e['cat']][] = $e; }
|
||||
foreach ($netbackup as $e) { $buckets['nb'][$e['cat']][] = $e; }
|
||||
|
||||
foreach ($buckets as $src => $cats) {
|
||||
foreach ($cats as $cat => $list) {
|
||||
usort($list, fn($a, $b) => strcasecmp($a['vm'], $b['vm']));
|
||||
$buckets[$src][$cat] = $list;
|
||||
}
|
||||
}
|
||||
|
||||
$veeamTotal = count($veeam);
|
||||
$nbTotal = count($netbackup);
|
||||
|
||||
/* -------------------------------------------------------- 5. Rendering */
|
||||
|
||||
$blockMeta = [
|
||||
'error' => ['label' => 'Error', 'badge' => 'bg-danger', 'icon' => 'bi-x-octagon'],
|
||||
'warning' => ['label' => 'Warning', 'badge' => 'bg-warning text-dark', 'icon' => 'bi-exclamation-triangle'],
|
||||
'outdated' => ['label' => 'Outdated', 'badge' => 'bg-info text-dark', 'icon' => 'bi-clock-history'],
|
||||
'ok' => ['label' => 'OK', 'badge' => 'bg-success', 'icon' => 'bi-check-circle'],
|
||||
'excluded' => ['label' => 'Excluded', 'badge' => 'bg-secondary', 'icon' => 'bi-slash-circle'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Render one accordion item holding a bootstrap-table.
|
||||
* $extraLabel is "Duration" for Veeam and "Host" for NetBackup.
|
||||
*/
|
||||
function render_block($blockId, $meta, $rows, $extraLabel, $expanded = false) {
|
||||
$count = count($rows);
|
||||
$collapsed = $expanded ? '' : 'collapsed';
|
||||
$show = $expanded ? 'show' : '';
|
||||
$aria = $expanded ? 'true' : 'false';
|
||||
?>
|
||||
<div class="accordion-item">
|
||||
<h1 class="accordion-header">
|
||||
<button class="accordion-button <?php echo $collapsed; ?>" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#c_<?php echo $blockId; ?>"
|
||||
aria-expanded="<?php echo $aria; ?>" <?php echo $count ? '' : 'disabled'; ?>>
|
||||
<i class="bi <?php echo $meta['icon']; ?> me-2"></i>
|
||||
<span class="block-title"><?php echo $meta['label']; ?></span>
|
||||
<span class="badge <?php echo $meta['badge']; ?> ms-3 block-count"><?php echo $count; ?></span>
|
||||
</button>
|
||||
</h1>
|
||||
<div id="c_<?php echo $blockId; ?>" class="accordion-collapse collapse <?php echo $show; ?>">
|
||||
<div class="accordion-body p-2">
|
||||
<?php if ($count === 0) : ?>
|
||||
<p class="text-muted mb-0">Aucune VM dans cette catégorie.</p>
|
||||
<?php else : ?>
|
||||
<table class="table table-bordered table-hover table-sm report-table"
|
||||
id="t_<?php echo $blockId; ?>" data-sortable="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="vm" data-sortable="true"><?php echo $GLOBALS['w_VMs']; ?></th>
|
||||
<th data-field="lastrun" data-sortable="true"><?php echo $GLOBALS['w_backuplu']; ?></th>
|
||||
<th data-field="status" data-sortable="true"><?php echo $GLOBALS['w_lastResult']; ?></th>
|
||||
<th data-field="lastgood" data-sortable="true"><?php echo $GLOBALS['w_lastGoodBackup']; ?></th>
|
||||
<th data-field="size" data-sortable="true"><?php echo $GLOBALS['w_size']; ?> (GB)</th>
|
||||
<th data-field="extra" data-sortable="true"><?php echo $extraLabel; ?></th>
|
||||
<th data-field="policy" data-sortable="true"><?php echo $GLOBALS['w_policy']; ?></th>
|
||||
<th data-field="message" data-sortable="true">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r) : ?>
|
||||
<tr class="<?php echo $r['row_class']; ?>">
|
||||
<td><b><?php echo htmlspecialchars($r['vm']); ?></b></td>
|
||||
<td><?php echo htmlspecialchars($r['last_run']); ?></td>
|
||||
<td><?php echo htmlspecialchars($r['status']); ?></td>
|
||||
<td><?php echo htmlspecialchars($r['last_good']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string) $r['size']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string) $r['extra']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string) $r['policy']); ?></td>
|
||||
<td style="font-size:.8em;"><i><?php echo htmlspecialchars((string) $r['message']); ?></i></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<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">
|
||||
<h2>
|
||||
<span class="badge text-bg-secondary w-100" id="ERR">
|
||||
VM Backup —
|
||||
<span class="text-light"><?php echo $veeamTotal; ?> Veeam</span> /
|
||||
<span class="text-light"><?php echo $nbTotal; ?> NetBackup</span> —
|
||||
<span class="text-danger"><?php echo count($buckets['veeam']['error']) + count($buckets['nb']['error']); ?> Error</span> -
|
||||
<span class="text-warning"><?php echo count($buckets['veeam']['warning']) + count($buckets['nb']['warning']); ?> Warning</span> -
|
||||
<span class="text-info"><?php echo count($buckets['veeam']['outdated']) + count($buckets['nb']['outdated']); ?> Outdated</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- ============================ VEEAM ============================ -->
|
||||
<h4 class="mt-3 mb-2">
|
||||
<i class="bi bi-shield-check me-1"></i> VEEAM
|
||||
<small class="text-muted"><?php echo $veeamTotal; ?> VMs</small>
|
||||
</h4>
|
||||
<div class="accordion" id="acc_veeam">
|
||||
<?php
|
||||
foreach (['error', 'warning', 'outdated', 'ok'] as $cat) {
|
||||
render_block('veeam_' . $cat, $blockMeta[$cat], $buckets['veeam'][$cat], 'Duration', false);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- ========================== NETBACKUP ========================== -->
|
||||
<h4 class="mt-4 mb-2">
|
||||
<i class="bi bi-hdd-stack me-1"></i> NETBACKUP
|
||||
<small class="text-muted"><?php echo $nbTotal; ?> VMs</small>
|
||||
</h4>
|
||||
<div class="accordion mb-4" id="acc_nb">
|
||||
<?php
|
||||
foreach (['error', 'warning', 'outdated', 'ok', 'excluded'] as $cat) {
|
||||
render_block('nb_' . $cat, $blockMeta[$cat], $buckets['nb'][$cat], $GLOBALS['w_host'], false);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/switch.js"></script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
// Tables are built lazily, after switch.js has already themed the page,
|
||||
// so they have to pick up the current theme themselves.
|
||||
function isDark() {
|
||||
return document.body.classList.contains('bg-dark');
|
||||
}
|
||||
|
||||
function syncTheme($table) {
|
||||
$table.toggleClass('table-dark', isDark());
|
||||
}
|
||||
|
||||
// Pin the table head right below its own (sticky) accordion header.
|
||||
function updateStickyOffset($item) {
|
||||
var h = $item.find('.accordion-header').outerHeight() || 56;
|
||||
$item.css('--acc-header-h', h + 'px');
|
||||
}
|
||||
|
||||
// bootstrap-table cannot measure a hidden container, so tables are
|
||||
// initialised the first time their accordion block is opened.
|
||||
function initTable($table) {
|
||||
if ($table.length === 0) return;
|
||||
if (!$table.data('bt-initialized')) {
|
||||
$table.bootstrapTable();
|
||||
$table.data('bt-initialized', true);
|
||||
} else {
|
||||
$table.bootstrapTable('resetView');
|
||||
}
|
||||
syncTheme($table);
|
||||
updateStickyOffset($table.closest('.accordion-item'));
|
||||
}
|
||||
|
||||
$('.accordion-collapse').on('shown.bs.collapse', function () {
|
||||
initTable($(this).find('table.report-table'));
|
||||
});
|
||||
|
||||
// Keep tables in sync when the light switch is toggled after load.
|
||||
$('#lightSwitch').on('change', function () {
|
||||
setTimeout(function () {
|
||||
$('table.report-table').each(function () { syncTheme($(this)); });
|
||||
}, 0);
|
||||
});
|
||||
|
||||
$(window).on('resize', function () {
|
||||
$('.accordion-item').each(function () { updateStickyOffset($(this)); });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
450
reports/Cluster_Compliance.php
Normal file
450
reports/Cluster_Compliance.php
Normal 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 — 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>Écart UBR (mê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">Écart</th>
|
||||
<th data-field="noeuds" data-sortable="true">Nœ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è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é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é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>Écart UBR — même build, KB à rattraper</h4>
|
||||
<p class="text-muted small mb-2">
|
||||
Ces clusters sont conformes au niveau cluster (build aligné), mais un ou plusieurs nœuds
|
||||
sont en retard de correctif. Triés par écart décroissant — les plus gros à corriger en priorité.
|
||||
</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œ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èle</th>
|
||||
<th data-field="bios" data-sortable="false">BIOS</th>
|
||||
<th data-field="detail" data-sortable="false">Dé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é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œ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è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">— <?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>
|
||||
221
reports/Cluster_Compliance_detail.php
Normal file
221
reports/Cluster_Compliance_detail.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<!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>
|
||||
code.sig { font-size: .85em; }
|
||||
.table td { vertical-align: middle; }
|
||||
.pivot th, .pivot td { text-align: center; }
|
||||
.pivot td.comp { text-align: left; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include $_SERVER['DOCUMENT_ROOT']."/include/all.php" ; ?>
|
||||
<div class="container-fluid">
|
||||
<div class="row flex-nowrap">
|
||||
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark" style="-ms-flex: 0 0 230px;flex: 0 0 230px;">
|
||||
<?php include $_SERVER['DOCUMENT_ROOT']."/navbar.html" ; ?>
|
||||
</div>
|
||||
<div class="col py-3">
|
||||
<?php
|
||||
// -----------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------
|
||||
function majority($arr) {
|
||||
if (count($arr) == 0) { return null; }
|
||||
$c = array_count_values($arr);
|
||||
arsort($c);
|
||||
return key($c);
|
||||
}
|
||||
|
||||
$cluster = isset($_GET['cluster']) ? $_GET['cluster'] : '';
|
||||
$run = isset($_GET['run']) ? $_GET['run'] : '';
|
||||
|
||||
$conn = DB_INFRA();
|
||||
|
||||
$nodes = array();
|
||||
$delta = array();
|
||||
$runDate = '-';
|
||||
|
||||
if ($cluster !== '' && $run !== '') {
|
||||
// --- Node facts (parameterized to avoid injection) -------
|
||||
$sqlNodes = "SELECT NodeName, OsName, MsPatchLevel, LastHotfix,
|
||||
CONVERT(VARCHAR(10), LastHotfixOn, 120) AS LastHotfixOn,
|
||||
SppSignature, Reachable, MsDrift, SppDrift, ErrorMessage,
|
||||
CONVERT(VARCHAR(16), RunDate, 120) AS RunDate
|
||||
FROM dbo.ClusterComplianceResults
|
||||
WHERE ClusterName = ? AND RunId = ?
|
||||
ORDER BY NodeName";
|
||||
$stmt = odbc_prepare($conn, $sqlNodes);
|
||||
if ($stmt && odbc_execute($stmt, array($cluster, $run))) {
|
||||
while ($r = odbc_fetch_array($stmt)) {
|
||||
if ($runDate === '-' && !empty($r['RunDate'])) { $runDate = $r['RunDate']; }
|
||||
$nodes[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// --- SPP component delta ---------------------------------
|
||||
$sqlDelta = "SELECT ComponentName, NodeName, ComponentVersion
|
||||
FROM dbo.ClusterComplianceSppDelta
|
||||
WHERE ClusterName = ? AND RunId = ?
|
||||
ORDER BY ComponentName, NodeName";
|
||||
$stmt2 = odbc_prepare($conn, $sqlDelta);
|
||||
if ($stmt2 && odbc_execute($stmt2, array($cluster, $run))) {
|
||||
while ($r = odbc_fetch_array($stmt2)) { $delta[] = $r; }
|
||||
}
|
||||
}
|
||||
|
||||
// Build the delta pivot : component x node
|
||||
$deltaNodes = array();
|
||||
$comps = array();
|
||||
$matrix = array();
|
||||
foreach ($delta as $d) {
|
||||
$cp = $d['ComponentName'];
|
||||
$nd = $d['NodeName'];
|
||||
if (!in_array($nd, $deltaNodes)) { $deltaNodes[] = $nd; }
|
||||
$comps[$cp] = true;
|
||||
$matrix[$cp][$nd] = $d['ComponentVersion'];
|
||||
}
|
||||
sort($deltaNodes);
|
||||
$comps = array_keys($comps);
|
||||
sort($comps);
|
||||
|
||||
// MS majority for highlighting
|
||||
$msVals = array();
|
||||
foreach ($nodes as $n) {
|
||||
if ($n['Reachable'] == 1 && $n['MsPatchLevel'] !== '' && $n['MsPatchLevel'] !== null) {
|
||||
$msVals[] = $n['MsPatchLevel'];
|
||||
}
|
||||
}
|
||||
$msMajority = majority($msVals);
|
||||
?>
|
||||
|
||||
<h1>
|
||||
<span class="badge text-bg-secondary" style="width:100%;">
|
||||
Détail cluster : <?php echo htmlspecialchars($cluster); ?>
|
||||
<small><small>(<?php echo htmlspecialchars($runDate); ?>)</small></small>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<a href="cluster_compliance.php" class="btn btn-sm btn-outline-secondary mb-3">
|
||||
<i class="bi bi-arrow-left"></i> Retour au tableau de bord
|
||||
</a>
|
||||
|
||||
<?php if (count($nodes) == 0): ?>
|
||||
<div class="alert alert-warning">Aucune donnée pour ce cluster / ce run.</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- ============ Node facts ============ -->
|
||||
<h4 class="mt-3">État des nœuds</h4>
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nœud</th>
|
||||
<th>OS</th>
|
||||
<th>Niveau MS</th>
|
||||
<th>Signature SPP</th>
|
||||
<th>État</th>
|
||||
<th>Erreur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($nodes as $n): ?>
|
||||
<?php
|
||||
$msCls = '';
|
||||
if ($n['Reachable'] == 1 && $msMajority !== null && $n['MsPatchLevel'] !== $msMajority) {
|
||||
$msCls = 'table-warning';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($n['NodeName']); ?></td>
|
||||
<td><?php echo htmlspecialchars($n['OsName']); ?></td>
|
||||
<td class="<?php echo $msCls; ?>"><?php echo htmlspecialchars($n['MsPatchLevel']); ?></td>
|
||||
<td><code class="sig"><?php echo htmlspecialchars($n['SppSignature']); ?></code></td>
|
||||
<td>
|
||||
<?php if ($n['Reachable'] == 1): ?>
|
||||
<span class="badge text-bg-success">Joignable</span>
|
||||
<?php else: ?>
|
||||
<span class="badge text-bg-danger">Injoignable</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><small class="text-muted"><?php echo htmlspecialchars($n['ErrorMessage']); ?></small></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ============ SPP delta pivot ============ -->
|
||||
<h4 class="mt-4">Delta SPP HP (composants divergents)</h4>
|
||||
<?php if (count($comps) == 0): ?>
|
||||
<div class="alert alert-info mb-0">
|
||||
Aucun écart de composant HP relevé (drift Microsoft uniquement, ou signatures identiques).
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm pivot">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="comp">Composant</th>
|
||||
<?php foreach ($deltaNodes as $nd): ?>
|
||||
<th><?php echo htmlspecialchars($nd); ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($comps as $cp): ?>
|
||||
<?php
|
||||
$rowVals = array();
|
||||
foreach ($deltaNodes as $nd) {
|
||||
$rowVals[] = isset($matrix[$cp][$nd]) ? $matrix[$cp][$nd] : '<absent>';
|
||||
}
|
||||
$maj = majority($rowVals);
|
||||
?>
|
||||
<tr>
|
||||
<td class="comp"><?php echo htmlspecialchars($cp); ?></td>
|
||||
<?php foreach ($deltaNodes as $nd): ?>
|
||||
<?php
|
||||
$v = isset($matrix[$cp][$nd]) ? $matrix[$cp][$nd] : '<absent>';
|
||||
$cls = '';
|
||||
if ($v === '<absent>') { $cls = 'table-danger'; }
|
||||
elseif ($v !== $maj) { $cls = 'table-warning'; }
|
||||
?>
|
||||
<td class="<?php echo $cls; ?>"><?php echo htmlspecialchars($v); ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">
|
||||
<span class="badge text-bg-warning text-dark">jaune</span> version différente de la majorité
|
||||
<span class="badge text-bg-danger">rouge</span> composant absent du nœud
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; /* nodes exist */ ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/switch.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
941
reports/Netbackup_Daily.php
Normal file
941
reports/Netbackup_Daily.php
Normal file
@@ -0,0 +1,941 @@
|
||||
<?php ob_start();?>
|
||||
<!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 IT - NetBackup Daily</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>
|
||||
<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>
|
||||
.accordion,
|
||||
.accordion-item,
|
||||
.accordion-button,
|
||||
.accordion-button:not(.collapsed),
|
||||
.accordion-body {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
border-color: rgba(128, 128, 128, .35);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.accordion-button:not(.collapsed) {
|
||||
box-shadow: none;
|
||||
background-color: rgba(128, 128, 128, .12);
|
||||
}
|
||||
|
||||
.accordion-button:focus {
|
||||
box-shadow: none;
|
||||
border-color: rgba(128, 128, 128, .35);
|
||||
}
|
||||
|
||||
.accordion-button::after {
|
||||
filter: grayscale(1) opacity(.75);
|
||||
}
|
||||
|
||||
body.bg-dark .accordion-button::after {
|
||||
filter: invert(1) grayscale(1) opacity(.85);
|
||||
}
|
||||
|
||||
.accordion-body,
|
||||
.accordion-body .fixed-table-container,
|
||||
.accordion-body .fixed-table-body,
|
||||
.accordion-body .fixed-table-toolbar {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.accordion-body .fixed-table-container {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.report-table thead th {
|
||||
position: sticky;
|
||||
top: var(--acc-header-h, 56px);
|
||||
z-index: 2;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
body.bg-dark .accordion-header,
|
||||
body.bg-dark .report-table thead th {
|
||||
background-color: #212529;
|
||||
}
|
||||
|
||||
.block-count {
|
||||
font-size: 1.25rem;
|
||||
min-width: 3.2em;
|
||||
}
|
||||
|
||||
.accordion-button .block-title {
|
||||
font-size: 1.45rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
.master-card {
|
||||
border-left-width: 6px !important;
|
||||
border-left-style: solid !important;
|
||||
}
|
||||
|
||||
.master-ok { border-left-color: #198754 !important; }
|
||||
.master-warning { border-left-color: #ffc107 !important; }
|
||||
.master-critical { border-left-color: #dc3545 !important; }
|
||||
|
||||
.metric {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: .72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
min-height: 86px;
|
||||
}
|
||||
|
||||
.warning-size {
|
||||
color: #b7791f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body.bg-dark .warning-size {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
body.bg-dark .card {
|
||||
--bs-card-bg: #2b3035;
|
||||
--bs-card-color: #f8f9fa;
|
||||
--bs-card-border-color: #495057;
|
||||
background-color: #2b3035;
|
||||
color: #f8f9fa;
|
||||
border-color: #495057;
|
||||
}
|
||||
|
||||
body.bg-dark .master-card,
|
||||
body.bg-dark .summary-card {
|
||||
background-color: #2b3035 !important;
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
body.bg-dark .card .text-muted,
|
||||
body.bg-dark .master-card .text-muted,
|
||||
body.bg-dark .summary-card .text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
|
||||
body.bg-dark .metric-label {
|
||||
color: #adb5bd;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* More readable OK green, especially on the grey title badge. */
|
||||
.title-ok {
|
||||
color: #00d084 !important;
|
||||
}
|
||||
|
||||
body.bg-dark .title-ok {
|
||||
color: #5ee6a8 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.accordion-button .block-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.block-count {
|
||||
font-size: 1rem;
|
||||
min-width: 2.8em;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
font-size: .82rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light text-dark">
|
||||
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
|
||||
|
||||
<?php
|
||||
/* ============================================================================
|
||||
Helpers
|
||||
========================================================================== */
|
||||
|
||||
function h($value) {
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function db_int($row, $key) {
|
||||
return isset($row[$key]) && $row[$key] !== null ? (int)$row[$key] : 0;
|
||||
}
|
||||
|
||||
function fmt_dt($value) {
|
||||
if (empty($value)) return '-';
|
||||
$ts = strtotime((string)$value);
|
||||
return $ts ? date('d/m/Y H:i', $ts) : '-';
|
||||
}
|
||||
|
||||
function fmt_gb_from_kb($kb) {
|
||||
if ($kb === null || $kb === '') return '-';
|
||||
return number_format(((float)$kb) / 1048576, 1, ',', ' ') . ' GB';
|
||||
}
|
||||
|
||||
// PURGE Random datas //
|
||||
Invoke_Infra("delete from netbackupjobs where master not like '%-%'");
|
||||
|
||||
/* ============================================================================
|
||||
SQL commun : point de départ par master
|
||||
Dernier jour (dans les 3 jours precedents) ayant un Infra/full,
|
||||
puis premier JobID Infra/full de ce jour.
|
||||
========================================================================== */
|
||||
|
||||
$startPointCte = "
|
||||
WITH Masters AS
|
||||
(
|
||||
SELECT DISTINCT master
|
||||
FROM dbo.NetBackupJobs
|
||||
),
|
||||
LatestFullDay AS
|
||||
(
|
||||
SELECT
|
||||
master,
|
||||
MAX(CAST(start_date AS date)) AS full_day
|
||||
FROM dbo.NetBackupJobs
|
||||
WHERE
|
||||
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
|
||||
AND start_date < CAST(GETDATE() AS date)
|
||||
AND policy LIKE '%Infra%'
|
||||
AND schedule_type = 'full'
|
||||
GROUP BY master
|
||||
),
|
||||
StartPoints AS
|
||||
(
|
||||
SELECT
|
||||
j.master,
|
||||
MIN(j.job) AS start_job
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN LatestFullDay d
|
||||
ON d.master = j.master
|
||||
AND CAST(j.start_date AS date) = d.full_day
|
||||
WHERE
|
||||
j.policy LIKE '%Infra%'
|
||||
AND j.schedule_type = 'full'
|
||||
GROUP BY j.master
|
||||
)
|
||||
";
|
||||
|
||||
/* ============================================================================
|
||||
1. Résumé par master
|
||||
========================================================================== */
|
||||
|
||||
$summarySql = $startPointCte . ",
|
||||
Jobs AS
|
||||
(
|
||||
SELECT j.*
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN StartPoints s
|
||||
ON s.master = j.master
|
||||
AND j.job >= s.start_job
|
||||
),
|
||||
LargeIncrementals AS
|
||||
(
|
||||
SELECT
|
||||
j.master,
|
||||
COUNT(DISTINCT j.client) AS large_incremental_clients
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN StartPoints s
|
||||
ON s.master = j.master
|
||||
AND j.job >= s.start_job
|
||||
WHERE
|
||||
j.schedule_type = 'incremental'
|
||||
AND j.status = 0
|
||||
AND j.policy NOT LIKE '%Hyper%'
|
||||
AND j.policy NOT LIKE '%standard%'
|
||||
AND j.policy NOT LIKE '%WE%'
|
||||
AND j.size_kb > 73400320
|
||||
GROUP BY j.master
|
||||
)
|
||||
SELECT
|
||||
m.master,
|
||||
s.start_job,
|
||||
|
||||
COUNT(j.job) AS total_jobs,
|
||||
|
||||
COALESCE(SUM(CASE WHEN j.status <= 1 THEN 1 ELSE 0 END), 0) AS jobs_ok,
|
||||
COALESCE(SUM(CASE WHEN j.status > 1 THEN 1 ELSE 0 END), 0) AS jobs_error,
|
||||
|
||||
COALESCE(SUM(
|
||||
CASE WHEN j.schedule_type IN ('full','incremental') THEN 1 ELSE 0 END
|
||||
), 0) AS scheduled_jobs,
|
||||
|
||||
COALESCE(SUM(
|
||||
CASE WHEN j.schedule_type IN ('full','incremental')
|
||||
AND j.status > 1 THEN 1 ELSE 0 END
|
||||
), 0) AS scheduled_errors,
|
||||
|
||||
COALESCE(SUM(
|
||||
CASE WHEN j.schedule_type = 'user backup' THEN 1 ELSE 0 END
|
||||
), 0) AS user_jobs,
|
||||
|
||||
COALESCE(SUM(
|
||||
CASE WHEN j.schedule_type = 'user backup'
|
||||
AND j.status > 1 THEN 1 ELSE 0 END
|
||||
), 0) AS user_errors,
|
||||
|
||||
COALESCE(li.large_incremental_clients, 0) AS large_incremental_clients
|
||||
|
||||
FROM Masters m
|
||||
LEFT JOIN StartPoints s
|
||||
ON s.master = m.master
|
||||
LEFT JOIN Jobs j
|
||||
ON j.master = m.master
|
||||
LEFT JOIN LargeIncrementals li
|
||||
ON li.master = m.master
|
||||
GROUP BY
|
||||
m.master,
|
||||
s.start_job,
|
||||
li.large_incremental_clients
|
||||
ORDER BY m.master
|
||||
";
|
||||
|
||||
$summary = Invoke_Infra($summarySql);
|
||||
if (!is_array($summary)) $summary = [];
|
||||
|
||||
/* ============================================================================
|
||||
2. Jobs en erreur + description NBRC
|
||||
========================================================================== */
|
||||
|
||||
$errorSql = "
|
||||
WITH LatestFullDay AS
|
||||
(
|
||||
SELECT
|
||||
master,
|
||||
MAX(CAST(start_date AS date)) AS full_day
|
||||
FROM dbo.NetBackupJobs
|
||||
WHERE
|
||||
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
|
||||
AND start_date < CAST(GETDATE() AS date)
|
||||
AND policy LIKE '%Infra%'
|
||||
AND schedule_type = 'full'
|
||||
GROUP BY master
|
||||
),
|
||||
StartPoints AS
|
||||
(
|
||||
SELECT
|
||||
j.master,
|
||||
MIN(j.job) AS start_job
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN LatestFullDay d
|
||||
ON d.master = j.master
|
||||
AND CAST(j.start_date AS date) = d.full_day
|
||||
WHERE
|
||||
j.policy LIKE '%Infra%'
|
||||
AND j.schedule_type = 'full'
|
||||
GROUP BY j.master
|
||||
)
|
||||
SELECT
|
||||
j.master,
|
||||
j.job,
|
||||
j.client,
|
||||
j.policy,
|
||||
j.schedule_type,
|
||||
j.status,
|
||||
r.RC_DESC AS status_desc,
|
||||
j.start_date,
|
||||
j.end_date,
|
||||
j.size_kb
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN StartPoints s
|
||||
ON s.master = j.master
|
||||
AND j.job >= s.start_job
|
||||
LEFT JOIN dbo.NBRC r
|
||||
ON r.RC = j.status
|
||||
WHERE
|
||||
j.status > 1
|
||||
ORDER BY
|
||||
j.master,
|
||||
CASE
|
||||
WHEN j.schedule_type IN ('full','incremental') THEN 0
|
||||
WHEN j.schedule_type = 'user backup' THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
j.client,
|
||||
j.job
|
||||
";
|
||||
|
||||
$errors = Invoke_Infra($errorSql);
|
||||
if (!is_array($errors)) $errors = [];
|
||||
|
||||
/* ============================================================================
|
||||
3. Warnings : incrémentales > 70 GB
|
||||
Règle métier exacte :
|
||||
- incremental
|
||||
- status = 0
|
||||
- policy NOT LIKE Hyper / standard / WE
|
||||
- size > 70 GB
|
||||
- à partir du point de départ du master
|
||||
|
||||
On garde une ligne par master/client, correspondant au plus gros job du client.
|
||||
========================================================================== */
|
||||
|
||||
$largeSql = "
|
||||
WITH LatestFullDay AS
|
||||
(
|
||||
SELECT
|
||||
master,
|
||||
MAX(CAST(start_date AS date)) AS full_day
|
||||
FROM dbo.NetBackupJobs
|
||||
WHERE
|
||||
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
|
||||
AND start_date < CAST(GETDATE() AS date)
|
||||
AND policy LIKE '%Infra%'
|
||||
AND schedule_type = 'full'
|
||||
GROUP BY master
|
||||
),
|
||||
StartPoints AS
|
||||
(
|
||||
SELECT
|
||||
j.master,
|
||||
MIN(j.job) AS start_job
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN LatestFullDay d
|
||||
ON d.master = j.master
|
||||
AND CAST(j.start_date AS date) = d.full_day
|
||||
WHERE
|
||||
j.policy LIKE '%Infra%'
|
||||
AND j.schedule_type = 'full'
|
||||
GROUP BY j.master
|
||||
),
|
||||
Candidates AS
|
||||
(
|
||||
SELECT
|
||||
j.*,
|
||||
ROW_NUMBER() OVER
|
||||
(
|
||||
PARTITION BY j.master, j.client
|
||||
ORDER BY j.size_kb DESC, j.job DESC
|
||||
) AS rn
|
||||
FROM dbo.NetBackupJobs j
|
||||
INNER JOIN StartPoints s
|
||||
ON s.master = j.master
|
||||
AND j.job >= s.start_job
|
||||
WHERE
|
||||
j.schedule_type = 'incremental'
|
||||
AND j.status = 0
|
||||
AND j.policy NOT LIKE '%Hyper%'
|
||||
AND j.policy NOT LIKE '%standard%'
|
||||
AND j.policy NOT LIKE '%WE%'
|
||||
AND j.size_kb > 73400320
|
||||
)
|
||||
SELECT
|
||||
master,
|
||||
job,
|
||||
client,
|
||||
policy,
|
||||
schedule_type,
|
||||
status,
|
||||
start_date,
|
||||
end_date,
|
||||
size_kb
|
||||
FROM Candidates
|
||||
WHERE rn = 1
|
||||
ORDER BY master, size_kb DESC, client
|
||||
";
|
||||
|
||||
$largeIncrementals = Invoke_Infra($largeSql);
|
||||
if (!is_array($largeIncrementals)) $largeIncrementals = [];
|
||||
|
||||
/* ============================================================================
|
||||
Préparation des données
|
||||
========================================================================== */
|
||||
|
||||
$scheduledErrors = [];
|
||||
$userErrors = [];
|
||||
|
||||
foreach ($errors as $row) {
|
||||
$type = strtolower(trim((string)($row['schedule_type'] ?? '')));
|
||||
|
||||
if ($type === 'full' || $type === 'incremental') {
|
||||
$scheduledErrors[] = $row;
|
||||
} elseif ($type === 'user backup') {
|
||||
$userErrors[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$mastersOk = 0;
|
||||
$mastersWarning = 0;
|
||||
$mastersCritical = 0;
|
||||
$totalJobs = 0;
|
||||
$totalErrors = 0;
|
||||
|
||||
foreach ($summary as &$row) {
|
||||
$total = db_int($row, 'total_jobs');
|
||||
$ok = db_int($row, 'jobs_ok');
|
||||
$err = db_int($row, 'jobs_error');
|
||||
$large = db_int($row, 'large_incremental_clients');
|
||||
$noStart = !isset($row['start_job']) || $row['start_job'] === null || $row['start_job'] === '';
|
||||
|
||||
$totalJobs += $total;
|
||||
$totalErrors += $err;
|
||||
|
||||
if ($noStart || $total === 0 || ($total > 0 && $ok === 0)) {
|
||||
$row['_state'] = 'critical';
|
||||
$mastersCritical++;
|
||||
|
||||
if ($noStart) {
|
||||
$row['_state_text'] = 'Aucun point de départ Infra trouvé';
|
||||
} elseif ($total === 0) {
|
||||
$row['_state_text'] = 'Aucune sauvegarde détectée';
|
||||
} else {
|
||||
$row['_state_text'] = 'Toutes les sauvegardes sont en erreur';
|
||||
}
|
||||
} elseif ($err > 0 || $large > 0) {
|
||||
$row['_state'] = 'warning';
|
||||
$mastersWarning++;
|
||||
|
||||
$parts = [];
|
||||
if ($err > 0) $parts[] = $err . ' erreur(s)';
|
||||
if ($large > 0) $parts[] = $large . ' incr. > 70 GB';
|
||||
$row['_state_text'] = implode(' / ', $parts);
|
||||
} else {
|
||||
$row['_state'] = 'ok';
|
||||
$row['_state_text'] = 'OK';
|
||||
$mastersOk++;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$masterCount = count($summary);
|
||||
$totalLarge = count($largeIncrementals);
|
||||
|
||||
/* ============================================================================
|
||||
Helpers de rendu
|
||||
========================================================================== */
|
||||
|
||||
function render_master_card($m) {
|
||||
$state = $m['_state'] ?? 'ok';
|
||||
|
||||
$borderClass = [
|
||||
'ok' => 'master-ok',
|
||||
'warning' => 'master-warning',
|
||||
'critical' => 'master-critical',
|
||||
][$state];
|
||||
|
||||
$badgeClass = [
|
||||
'ok' => 'text-bg-success',
|
||||
'warning' => 'bg-warning text-dark',
|
||||
'critical' => 'text-bg-danger',
|
||||
][$state];
|
||||
|
||||
$largeClass = db_int($m, 'large_incremental_clients') > 0 ? 'text-warning' : 'text-success';
|
||||
?>
|
||||
<div class="card shadow-sm mb-2 master-card <?php echo $borderClass; ?>">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<div>
|
||||
<div class="fw-bold fs-5"><?php echo h($m['master'] ?? ''); ?></div>
|
||||
<span class="badge <?php echo $badgeClass; ?>">
|
||||
<?php echo h($m['_state_text'] ?? ''); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted">
|
||||
Start job :
|
||||
<strong><?php echo h($m['start_job'] ?? '-'); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (db_int($m, 'total_jobs') > 0) : ?>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-4 col-md-2">
|
||||
<div class="metric"><?php echo db_int($m, 'total_jobs'); ?></div>
|
||||
<div class="metric-label">Total</div>
|
||||
</div>
|
||||
<div class="col-4 col-md-2">
|
||||
<div class="metric text-success"><?php echo db_int($m, 'jobs_ok'); ?></div>
|
||||
<div class="metric-label">OK</div>
|
||||
</div>
|
||||
<div class="col-4 col-md-2">
|
||||
<div class="metric text-danger"><?php echo db_int($m, 'jobs_error'); ?></div>
|
||||
<div class="metric-label">Erreurs</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="metric">
|
||||
<?php echo db_int($m, 'scheduled_errors'); ?> /
|
||||
<?php echo db_int($m, 'scheduled_jobs'); ?>
|
||||
</div>
|
||||
<div class="metric-label">Full / Incr.</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="metric">
|
||||
<?php echo db_int($m, 'user_errors'); ?> /
|
||||
<?php echo db_int($m, 'user_jobs'); ?>
|
||||
</div>
|
||||
<div class="metric-label">User backup</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 small">
|
||||
Incr. > 70 GB :
|
||||
<strong class="<?php echo $largeClass; ?>">
|
||||
<?php echo db_int($m, 'large_incremental_clients'); ?>
|
||||
</strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function render_error_table($id, $rows, $emptyText = 'Aucun job dans cette catégorie.') {
|
||||
?>
|
||||
<?php if (count($rows) === 0) : ?>
|
||||
<p class="text-muted mb-0"><?php echo h($emptyText); ?></p>
|
||||
<?php else : ?>
|
||||
<table
|
||||
class="table table-bordered table-hover table-sm report-table"
|
||||
id="<?php echo h($id); ?>"
|
||||
data-sortable="true"
|
||||
data-search="true"
|
||||
data-show-export="true"
|
||||
data-export-types='["csv","excel"]'
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="master" data-sortable="true">Master</th>
|
||||
<th data-field="client" data-sortable="true">Client</th>
|
||||
<th data-field="policy" data-sortable="true">Policy</th>
|
||||
<th data-field="schedule" data-sortable="true">Schedule</th>
|
||||
<th data-field="status" data-sortable="true">Status</th>
|
||||
<th data-field="description" data-sortable="true">Description</th>
|
||||
<th data-field="start" data-sortable="true">Début</th>
|
||||
<th data-field="end" data-sortable="true">Fin</th>
|
||||
<th data-field="size" data-sortable="true">Taille</th>
|
||||
<th data-field="job" data-sortable="true">Job</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r) : ?>
|
||||
<tr class="table-danger">
|
||||
<td><?php echo h($r['master'] ?? ''); ?></td>
|
||||
<td><strong><?php echo h($r['client'] ?? ''); ?></strong></td>
|
||||
<td><?php echo h($r['policy'] ?? ''); ?></td>
|
||||
<td><?php echo h($r['schedule_type'] ?? ''); ?></td>
|
||||
<td><strong><?php echo h($r['status'] ?? ''); ?></strong></td>
|
||||
<td><?php echo h($r['status_desc'] ?? 'Description non trouvée'); ?></td>
|
||||
<td><?php echo h(fmt_dt($r['start_date'] ?? null)); ?></td>
|
||||
<td><?php echo h(fmt_dt($r['end_date'] ?? null)); ?></td>
|
||||
<td><?php echo h(fmt_gb_from_kb($r['size_kb'] ?? null)); ?></td>
|
||||
<td><?php echo h($r['job'] ?? ''); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif;
|
||||
}
|
||||
|
||||
function render_warning_table($id, $rows) {
|
||||
?>
|
||||
<?php if (count($rows) === 0) : ?>
|
||||
<p class="text-muted mb-0">Aucune incrémentale > 70 GB.</p>
|
||||
<?php else : ?>
|
||||
<table
|
||||
class="table table-bordered table-hover table-sm report-table"
|
||||
id="<?php echo h($id); ?>"
|
||||
data-sortable="true"
|
||||
data-search="true"
|
||||
data-show-export="true"
|
||||
data-export-types='["csv","excel"]'
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="master" data-sortable="true">Master</th>
|
||||
<th data-field="client" data-sortable="true">Client</th>
|
||||
<th data-field="policy" data-sortable="true">Policy</th>
|
||||
<th data-field="size" data-sortable="true">Taille</th>
|
||||
<th data-field="status" data-sortable="true">Status</th>
|
||||
<th data-field="start" data-sortable="true">Début</th>
|
||||
<th data-field="end" data-sortable="true">Fin</th>
|
||||
<th data-field="job" data-sortable="true">Job</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r) : ?>
|
||||
<tr class="table-warning">
|
||||
<td><?php echo h($r['master'] ?? ''); ?></td>
|
||||
<td><strong><?php echo h($r['client'] ?? ''); ?></strong></td>
|
||||
<td><?php echo h($r['policy'] ?? ''); ?></td>
|
||||
<td class="warning-size"><?php echo h(fmt_gb_from_kb($r['size_kb'] ?? null)); ?></td>
|
||||
<td><span class="badge text-bg-success"><?php echo h($r['status'] ?? '0'); ?> - OK</span></td>
|
||||
<td><?php echo h(fmt_dt($r['start_date'] ?? null)); ?></td>
|
||||
<td><?php echo h(fmt_dt($r['end_date'] ?? null)); ?></td>
|
||||
<td><?php echo h($r['job'] ?? ''); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif;
|
||||
}
|
||||
|
||||
function render_accordion_block($id, $title, $icon, $badgeClass, $count, $renderer, $expanded = false) {
|
||||
$collapsed = $expanded ? '' : 'collapsed';
|
||||
$show = $expanded ? 'show' : '';
|
||||
$aria = $expanded ? 'true' : 'false';
|
||||
?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button <?php echo $collapsed; ?>"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#c_<?php echo h($id); ?>"
|
||||
aria-expanded="<?php echo $aria; ?>">
|
||||
<i class="bi <?php echo h($icon); ?> me-2"></i>
|
||||
<span class="block-title"><?php echo h($title); ?></span>
|
||||
<span class="badge <?php echo h($badgeClass); ?> ms-3 block-count"><?php echo (int)$count; ?></span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="c_<?php echo h($id); ?>" class="accordion-collapse collapse <?php echo $show; ?>">
|
||||
<div class="accordion-body p-2">
|
||||
<?php $renderer(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- ================================================================
|
||||
Titre global
|
||||
================================================================ -->
|
||||
|
||||
<h2>
|
||||
<span class="badge text-bg-secondary w-100 text-wrap" id="ERR">
|
||||
NetBackup Daily —
|
||||
<span class="text-light"><?php echo $masterCount; ?> masters</span> —
|
||||
<span class="title-ok"><?php echo $mastersOk; ?> OK</span> -
|
||||
<span class="text-warning"><?php echo $mastersWarning; ?> Warning</span> -
|
||||
<span class="text-danger"><?php echo $mastersCritical; ?> Critical</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
|
||||
<!-- ================================================================
|
||||
Compteurs globaux
|
||||
================================================================ -->
|
||||
|
||||
<div class="row g-2 mt-1 mb-3">
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm summary-card">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-3 fw-bold"><?php echo $totalJobs; ?></div>
|
||||
<div class="text-muted small">Jobs analysés</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm summary-card">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-3 fw-bold text-danger"><?php echo $totalErrors; ?></div>
|
||||
<div class="text-muted small">Jobs en erreur</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm summary-card">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-3 fw-bold text-warning"><?php echo $totalLarge; ?></div>
|
||||
<div class="text-muted small">Incr. > 70 GB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm summary-card">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-3 fw-bold text-danger"><?php echo $mastersCritical; ?></div>
|
||||
<div class="text-muted small">Masters critiques</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
Résumé par master
|
||||
================================================================ -->
|
||||
|
||||
<h4 class="mt-3 mb-2">
|
||||
<i class="bi bi-hdd-stack me-1"></i>
|
||||
Résumé par master
|
||||
</h4>
|
||||
|
||||
<div class="row">
|
||||
<?php foreach ($summary as $m) : ?>
|
||||
<div class="col-12 col-xl-6">
|
||||
<?php render_master_card($m); ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
Détails
|
||||
================================================================ -->
|
||||
|
||||
<h4 class="mt-4 mb-2">
|
||||
<i class="bi bi-list-check me-1"></i>
|
||||
Détails
|
||||
</h4>
|
||||
|
||||
<div class="accordion mb-4" id="acc_netbackup_daily">
|
||||
|
||||
<?php
|
||||
render_accordion_block(
|
||||
'scheduled_errors',
|
||||
'Full / Incremental en erreur',
|
||||
'bi-x-octagon',
|
||||
'bg-danger',
|
||||
count($scheduledErrors),
|
||||
function () use ($scheduledErrors) {
|
||||
render_error_table('t_scheduled_errors', $scheduledErrors);
|
||||
},
|
||||
count($scheduledErrors) > 0
|
||||
);
|
||||
|
||||
render_accordion_block(
|
||||
'user_errors',
|
||||
'User Backup en erreur',
|
||||
'bi-person-exclamation',
|
||||
'bg-danger',
|
||||
count($userErrors),
|
||||
function () use ($userErrors) {
|
||||
render_error_table('t_user_errors', $userErrors);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
render_accordion_block(
|
||||
'large_incrementals',
|
||||
'Incrémentales > 70 GB',
|
||||
'bi-exclamation-triangle',
|
||||
'bg-warning text-dark',
|
||||
count($largeIncrementals),
|
||||
function () use ($largeIncrementals) {
|
||||
render_warning_table('t_large_incrementals', $largeIncrementals);
|
||||
},
|
||||
count($largeIncrementals) > 0
|
||||
);
|
||||
?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/switch.js"></script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
function isDark() {
|
||||
return document.body.classList.contains('bg-dark');
|
||||
}
|
||||
|
||||
function syncTheme($table) {
|
||||
$table.toggleClass('table-dark', isDark());
|
||||
}
|
||||
|
||||
function updateStickyOffset($item) {
|
||||
var h = $item.find('.accordion-header').outerHeight() || 56;
|
||||
$item.css('--acc-header-h', h + 'px');
|
||||
}
|
||||
|
||||
function initTable($table) {
|
||||
if ($table.length === 0) return;
|
||||
|
||||
if (!$table.data('bt-initialized')) {
|
||||
$table.bootstrapTable();
|
||||
$table.data('bt-initialized', true);
|
||||
} else {
|
||||
$table.bootstrapTable('resetView');
|
||||
}
|
||||
|
||||
syncTheme($table);
|
||||
updateStickyOffset($table.closest('.accordion-item'));
|
||||
}
|
||||
|
||||
$('.accordion-collapse').on('shown.bs.collapse', function () {
|
||||
initTable($(this).find('table.report-table'));
|
||||
});
|
||||
|
||||
// Initialise aussi les accordéons ouverts par défaut au chargement.
|
||||
$('.accordion-collapse.show').each(function () {
|
||||
initTable($(this).find('table.report-table'));
|
||||
});
|
||||
|
||||
$('#lightSwitch').on('change', function () {
|
||||
setTimeout(function () {
|
||||
$('table.report-table').each(function () {
|
||||
syncTheme($(this));
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
$(window).on('resize', function () {
|
||||
$('.accordion-item').each(function () {
|
||||
updateStickyOffset($(this));
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,170 +6,391 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Page Title -->
|
||||
<title>Infra Reports IT</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
|
||||
|
||||
<!-- JQuery -->
|
||||
<script src="/js/jquery-3.6.1.min.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<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>
|
||||
<!-- Bootstrap-tables -->
|
||||
<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>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js" defer></script>
|
||||
<script src="/js/jquery-3.6.1.min.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-light text-dark">
|
||||
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
|
||||
<?php // DATA
|
||||
$windows = Invoke_Infra("select * from cmdb_srvall where DECOM is null and hostname not in (select server from maintenance_status where scom = 'Y' or zabbix = 'Y') and hostname <> '' order by hostname asc");
|
||||
$linux = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type = 'LINUX' order by h.hostname");
|
||||
$aix = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type = 'AIX' order by h.hostname");
|
||||
$other = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type is null order by h.hostname");
|
||||
$lok = "<div class='row'>";$lko = "<div class='row'>";$lcount=0;
|
||||
foreach($linux as $s){
|
||||
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
|
||||
$lko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since ".explode(".",$s['PREVTIME'])[0]."</span></div>";
|
||||
$lcount++;
|
||||
}else{
|
||||
$lok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
|
||||
}
|
||||
}
|
||||
if($lcount){$color = "DarkOrange";$msg = $lcount." issues";}else{$color = "green";$msg = "OK";}
|
||||
$aok = "<div class='row'>";$ako = "<div class='row'>";$acount=0;
|
||||
foreach($aix as $s){
|
||||
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
|
||||
$ako .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since".explode(".",$s['PREVTIME'])[0]."</span></div>";
|
||||
$acount++;
|
||||
}else{
|
||||
$aok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
|
||||
}
|
||||
}
|
||||
$ook = "<div class='row'>";$oko = "<div class='row'>";$ocount=0;
|
||||
foreach($other as $s){
|
||||
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
|
||||
$oko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since".explode(".",$s['PREVTIME'])[0]."</span></div>";
|
||||
$ocount++;
|
||||
}else{
|
||||
$ook .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
|
||||
}
|
||||
}
|
||||
$wok = "<div class='row'>";$wko = "<div class='row'>";$wcount=0;
|
||||
foreach($windows as $s){
|
||||
if($s['heartbeat'] < date('Y-m-d H:i:s', strtotime(' -16 minutes '))){
|
||||
$wko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['hostname'])." since ".explode(".",$s['heartbeat'])[0]."</span></div>";
|
||||
$wcount++;
|
||||
}else{
|
||||
$wok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['hostname']." ")."</span></div>";
|
||||
|
||||
<?php
|
||||
|
||||
include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php";
|
||||
|
||||
/**
|
||||
* Récupère une valeur dans un tableau sans dépendre de la casse
|
||||
* utilisée par le pilote ODBC.
|
||||
*/
|
||||
function getRowValue(array $row, string $key, $default = null)
|
||||
{
|
||||
if (array_key_exists($key, $row)) {
|
||||
return $row[$key];
|
||||
}
|
||||
|
||||
$upperKey = strtoupper($key);
|
||||
|
||||
if (array_key_exists($upperKey, $row)) {
|
||||
return $row[$upperKey];
|
||||
}
|
||||
|
||||
$lowerKey = strtolower($key);
|
||||
|
||||
if (array_key_exists($lowerKey, $row)) {
|
||||
return $row[$lowerKey];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les listes de serveurs OK et en erreur.
|
||||
*/
|
||||
function buildHeartbeatStatus(
|
||||
array $servers,
|
||||
string $hostnameKey,
|
||||
string $heartbeatKey,
|
||||
string $previousHeartbeatKey,
|
||||
string $limit
|
||||
): array {
|
||||
$okItems = [];
|
||||
$koItems = [];
|
||||
$issueCount = 0;
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$hostname = trim((string)getRowValue($server, $hostnameKey, ''));
|
||||
|
||||
if ($hostname === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$heartbeat = getRowValue($server, $heartbeatKey);
|
||||
$previousHeartbeat = getRowValue(
|
||||
$server,
|
||||
$previousHeartbeatKey,
|
||||
$heartbeat
|
||||
);
|
||||
|
||||
$hostnameDisplay = htmlspecialchars(
|
||||
strtoupper($hostname),
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
);
|
||||
|
||||
/*
|
||||
* Une date absente est considérée comme une erreur.
|
||||
* Les dates au format Y-m-d H:i:s peuvent être comparées
|
||||
* directement sous forme de chaînes.
|
||||
*/
|
||||
$isKo = empty($heartbeat) || $heartbeat < $limit;
|
||||
|
||||
if ($isKo) {
|
||||
$previousDisplay = 'unknown';
|
||||
|
||||
if (!empty($previousHeartbeat)) {
|
||||
$previousDisplay = explode('.', (string)$previousHeartbeat)[0];
|
||||
$previousDisplay = htmlspecialchars(
|
||||
$previousDisplay,
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
$koItems[] = "
|
||||
<div class=\"col-12 col-md-6 mb-1\">
|
||||
<span class=\"badge bg-danger text-wrap\">
|
||||
{$hostnameDisplay} since {$previousDisplay}
|
||||
</span>
|
||||
</div>
|
||||
";
|
||||
|
||||
$issueCount++;
|
||||
} else {
|
||||
$okItems[] = "
|
||||
<div class=\"col-6 col-md-4 col-xl-2 mb-1\">
|
||||
<span class=\"badge bg-success text-wrap\">
|
||||
{$hostnameDisplay}
|
||||
</span>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => implode('', $okItems),
|
||||
'ko' => implode('', $koItems),
|
||||
'issues' => $issueCount
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une section de l'accordéon.
|
||||
*/
|
||||
function displayHeartbeatSection(
|
||||
string $id,
|
||||
string $title,
|
||||
array $servers,
|
||||
array $status,
|
||||
string $deviceLabel
|
||||
): void {
|
||||
$count = count($servers);
|
||||
$issues = $status['issues'];
|
||||
|
||||
$backgroundColor = $issues > 0 ? 'DarkOrange' : 'green';
|
||||
|
||||
$message = $issues > 0
|
||||
? "<strong> → {$issues} issue(s)</strong>"
|
||||
: 'OK';
|
||||
|
||||
?>
|
||||
<div class="card mb-3" style="background-color: <?= $backgroundColor ?>;">
|
||||
<h4 class="card-header">
|
||||
<button
|
||||
class="btn text-white fs-3 text-start w-100"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
|
||||
aria-expanded="false"
|
||||
aria-controls="<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
|
||||
>
|
||||
<strong>
|
||||
<?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?> :
|
||||
<?= $count ?>
|
||||
<?= htmlspecialchars($deviceLabel, ENT_QUOTES, 'UTF-8') ?>
|
||||
<?= $message ?>
|
||||
</strong>
|
||||
</button>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
|
||||
class="accordion-collapse collapse"
|
||||
data-bs-parent="#accordion"
|
||||
>
|
||||
<div class="card card-body mb-3 bg-light">
|
||||
<?php if ($issues > 0): ?>
|
||||
<h5 class="mb-3">Issues</h5>
|
||||
|
||||
<div class="row">
|
||||
<?= $status['ko'] ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($status['ok'] !== ''): ?>
|
||||
<?php if ($issues > 0): ?>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
|
||||
<h5 class="mb-3">OK</h5>
|
||||
|
||||
<div class="row">
|
||||
<?= $status['ok'] ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/*
|
||||
* Seuils de heartbeat.
|
||||
*/
|
||||
$unixHeartbeatLimit = date(
|
||||
'Y-m-d H:i:s',
|
||||
time() - (11 * 60)
|
||||
);
|
||||
|
||||
$windowsHeartbeatLimit = date(
|
||||
'Y-m-d H:i:s',
|
||||
time() - (16 * 60)
|
||||
);
|
||||
|
||||
/*
|
||||
* Serveurs Windows.
|
||||
*/
|
||||
$windows = Invoke_Infra("
|
||||
SELECT
|
||||
c.hostname,
|
||||
c.heartbeat
|
||||
FROM cmdb_srvall c
|
||||
WHERE c.decom IS NULL
|
||||
AND c.hostname <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM maintenance_status m
|
||||
WHERE m.server = c.hostname
|
||||
AND (
|
||||
m.scom = 'Y'
|
||||
OR m.zabbix = 'Y'
|
||||
)
|
||||
)
|
||||
ORDER BY c.hostname ASC
|
||||
");
|
||||
|
||||
/*
|
||||
* Linux, AIX et autres :
|
||||
* une seule requête au lieu de trois.
|
||||
*/
|
||||
$unixServers = Invoke_aixcmdb("
|
||||
SELECT
|
||||
h.hostname,
|
||||
h.hbtime,
|
||||
h.prevtime,
|
||||
s.os_type
|
||||
FROM heartbeat h
|
||||
LEFT JOIN srvall s
|
||||
ON s.hostname = h.hostname
|
||||
WHERE h.mainttime IS NULL
|
||||
AND h.decomtime IS NULL
|
||||
AND (
|
||||
s.os_type IN ('LINUX', 'AIX')
|
||||
OR s.os_type IS NULL
|
||||
)
|
||||
ORDER BY h.hostname ASC
|
||||
");
|
||||
|
||||
/*
|
||||
* Répartition des résultats Unix.
|
||||
*/
|
||||
$linux = [];
|
||||
$aix = [];
|
||||
$other = [];
|
||||
|
||||
foreach ($unixServers as $server) {
|
||||
$osType = strtoupper(
|
||||
trim((string)getRowValue($server, 'os_type', ''))
|
||||
);
|
||||
|
||||
switch ($osType) {
|
||||
case 'LINUX':
|
||||
$linux[] = $server;
|
||||
break;
|
||||
|
||||
case 'AIX':
|
||||
$aix[] = $server;
|
||||
break;
|
||||
|
||||
default:
|
||||
$other[] = $server;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Génération des états.
|
||||
*/
|
||||
$windowsStatus = buildHeartbeatStatus(
|
||||
$windows,
|
||||
'hostname',
|
||||
'heartbeat',
|
||||
'heartbeat',
|
||||
$windowsHeartbeatLimit
|
||||
);
|
||||
|
||||
$linuxStatus = buildHeartbeatStatus(
|
||||
$linux,
|
||||
'hostname',
|
||||
'hbtime',
|
||||
'prevtime',
|
||||
$unixHeartbeatLimit
|
||||
);
|
||||
|
||||
$aixStatus = buildHeartbeatStatus(
|
||||
$aix,
|
||||
'hostname',
|
||||
'hbtime',
|
||||
'prevtime',
|
||||
$unixHeartbeatLimit
|
||||
);
|
||||
|
||||
$otherStatus = buildHeartbeatStatus(
|
||||
$other,
|
||||
'hostname',
|
||||
'hbtime',
|
||||
'prevtime',
|
||||
$unixHeartbeatLimit
|
||||
);
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<!-- HTML -->
|
||||
<div class="container-fluid" id="content">
|
||||
<div class="row flex-nowrap">
|
||||
<!-- Left NAVBAR -->
|
||||
<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;">
|
||||
|
||||
<!-- Left navbar -->
|
||||
<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>
|
||||
|
||||
<!-- Display -->
|
||||
<div class="col py-3">
|
||||
<!-- Page Title -->
|
||||
<h1><span class="badge text-bg-secondary font-weight-bold" style="width:100%;"><?php echo $ti_17;?></span></h1>
|
||||
<main class="col py-3">
|
||||
|
||||
<!-- Page title -->
|
||||
<h1>
|
||||
<span
|
||||
class="badge text-bg-secondary fw-bold"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<?= htmlspecialchars((string)$ti_17, ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="container">
|
||||
<!-- TABLE -->
|
||||
<div>
|
||||
<div id="accordion">
|
||||
<?php if($wcount){$color = "DarkOrange";$msg = "<b> --> ".$wcount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
|
||||
<div class="card" style='background-color:<?php echo $color ?>;'>
|
||||
<h4 class="card-header">
|
||||
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#windows"><b>Windows : <?php echo count($windows)." $w_devices ". $msg; ?></b></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="windows" class="collapse" data-bs-parent="#accordion">
|
||||
<?php echo "<br><h5>".$wko.'</div><br><br>'.$wok."</div></h5>"; ?>
|
||||
</div>
|
||||
<br>
|
||||
<div class="accordion" id="accordion">
|
||||
|
||||
<?php if($lcount){$color = "DarkOrange";$msg = "<b> --> ".$lcount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
|
||||
<div class="card" style='background-color:<?php echo $color ?>;'>
|
||||
<h4 class="card-header">
|
||||
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#linux"><b>Linux : <?php echo count($linux)." $w_devices ". $msg; ?></b></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="linux" class="collapse" data-bs-parent="#accordion">
|
||||
<?php echo "<br><h5>".$lko.'</div><br><br>'.$lok."</div></h5>"; ?>
|
||||
</div>
|
||||
<br>
|
||||
<?php
|
||||
displayHeartbeatSection(
|
||||
'windows',
|
||||
'Windows',
|
||||
$windows,
|
||||
$windowsStatus,
|
||||
$w_devices
|
||||
);
|
||||
|
||||
<?php if($acount){$color = "DarkOrange";$msg = "<b> --> ".$acount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
|
||||
<div class="card" style='background-color:<?php echo $color ?>;'>
|
||||
<h4 class="card-header text-white">
|
||||
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#aix"><b>AIX : <?php echo count($aix)." $w_devices ". $msg; ?></b></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="aix" class="collapse" data-bs-parent="#accordion">
|
||||
<?php echo "<br><h5>".$ako.'</div><br><br>'.$aok."</div></h5>"; ?>
|
||||
</div>
|
||||
<br>
|
||||
displayHeartbeatSection(
|
||||
'linux',
|
||||
'Linux',
|
||||
$linux,
|
||||
$linuxStatus,
|
||||
$w_devices
|
||||
);
|
||||
|
||||
<?php if($ocount){$color = "DarkOrange";$msg = "<b> --> ".$ocount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
|
||||
<div class="card" style='background-color:<?php echo $color ?>;'>
|
||||
<h4 class="card-header text-white">
|
||||
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#other"><b>Other : <?php echo count($other)." $w_devices ". $msg; ?></b></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="other" class="collapse" data-bs-parent="#accordion">
|
||||
<?php echo "<br><h5>".$oko.'</div><br><br>'.$ook."</div></h5>"; ?>
|
||||
</div>
|
||||
<br>
|
||||
displayHeartbeatSection(
|
||||
'aix',
|
||||
'AIX',
|
||||
$aix,
|
||||
$aixStatus,
|
||||
$w_devices
|
||||
);
|
||||
|
||||
displayHeartbeatSection(
|
||||
'other',
|
||||
'Other',
|
||||
$other,
|
||||
$otherStatus,
|
||||
$w_devices
|
||||
);
|
||||
?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of main content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</div>
|
||||
|
||||
<script src="/js/switch.js"></script>
|
||||
</HTML>
|
||||
|
||||
<SCRIPT>
|
||||
let table = $('#t1');
|
||||
$(document).ready(function() {
|
||||
table.DataTable({
|
||||
scrollY: '50vh',
|
||||
scrollCollapse: true,
|
||||
paging: false,
|
||||
});
|
||||
});
|
||||
|
||||
$(function () {
|
||||
let options = table.bootstrapTable('getOptions');
|
||||
options.height= document.getElementById('content').clientHeight-170;
|
||||
table.bootstrapTable('refreshOptions',options);
|
||||
});
|
||||
|
||||
function tableresize() {
|
||||
let options = table.bootstrapTable('getOptions');
|
||||
options.height= document.getElementById('content').clientHeight-170;
|
||||
table.bootstrapTable('refreshOptions',options);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", tableresize);
|
||||
</SCRIPT>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user