- 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.
411 lines
17 KiB
PHP
411 lines
17 KiB
PHP
<!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>
|