Files
Web-Infra-Reports-IT/reports/Backups.php
sva-e025532 c185d099d6 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.
2026-09-14 10:32:15 +02:00

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>