Files
Web-Infra-Reports-IT/reports/Backups.php
e025532 db223b805c Add Veeam and NetBackup support to inventory and reporting
- Introduced Veeam and NetBackup version tracking in `Inventory.php` for backup validation.
- Enhanced backup report in `Backups.php` with improved SQL queries and support for short hostname normalization.
- Updated `navbar.html` and related navigation components for better usability and accessibility.
- Refactored ActiveDirectory scripts for better modularity and event handling.
- Added `AGENTS.md` and `modele.php` as templates for documentation and new page creation.
2026-09-24 15:36:09 +02:00

554 lines
19 KiB
PHP

<?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</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 + NetBackupJobs (dun-bkp-01)
During the migration a VM can still exist in both backup sources.
It is displayed only on the side holding the most recent backup.
========================================================================== */
/* ---------------------------------------------------------------- helpers */
// NetBackup can store a client as FQDN while CMDB/Veeam often use a short name.
// Use the short hostname as the common comparison key.
function normalize_host($name) {
$name = strtoupper(trim((string) $name));
if ($name === '') return '';
$parts = explode('.', $name, 2);
return $parts[0];
}
// 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[normalize_host($r['name'])] = true; }
/* -------------------------------------------------------------- 1. Veeam */
$veeam = [];
$rows = Invoke_Infra("SELECT * FROM Veeam_Reporting");
foreach ($rows as $row) {
$key = normalize_host($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 */
/*
Source principale : dbo.NetBackupJobs
Master pris en compte : dun-bkp-01 uniquement.
Pour chaque client NetBackup :
- le dernier FULL/INCREMENTAL donne l'état courant ;
- le dernier FULL/INCREMENTAL avec status <= 1 donne "Last Good Backup" ;
- dbo.NBRC fournit la description du code retour.
La page reste une synthèse VM : seuls les clients présents dans cmdb_vms
avec un inventaire récent sont retenus.
*/
$netbackup = [];
$rows = Invoke_Infra("
WITH ScheduledJobs AS
(
SELECT
j.master,
j.job,
j.client,
j.policy,
j.schedule_type,
j.status,
j.start_date,
j.end_date,
j.size_kb,
COALESCE(j.end_date, j.start_date) AS run_date,
MAX(
CASE
WHEN j.status <= 1
THEN COALESCE(j.end_date, j.start_date)
ELSE NULL
END
) OVER (
PARTITION BY j.client
) AS last_success,
ROW_NUMBER() OVER
(
PARTITION BY j.client
ORDER BY
COALESCE(j.end_date, j.start_date) DESC,
j.job DESC
) AS rn
FROM dbo.NetBackupJobs j
WHERE
j.master like 'dun-bkp-01%'
AND j.schedule_type IN ('full', 'incremental')
),
Latest AS
(
SELECT *
FROM ScheduledJobs
WHERE rn = 1
)
SELECT
l.master,
l.job,
l.client,
l.policy,
l.schedule_type,
l.status,
l.start_date,
l.end_date,
l.run_date,
l.last_success,
l.size_kb,
r.RC_DESC AS status_desc
FROM Latest l
LEFT JOIN dbo.NBRC r
ON r.RC = l.status
ORDER BY l.client
");
if (!is_array($rows)) $rows = [];
foreach ($rows as $row) {
$key = normalize_host($row['client']);
// This synthesis page is VM-oriented.
if ($key === '' || !isset($active[$key])) continue;
$statusCode = (int) $row['status'];
$ageGood = days_since($row['last_success']);
if ($statusCode > 1) {
// Same policy as before:
// a failed latest run remains only a warning while a recent good copy exists.
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';
}
$statusDesc = trim((string)($row['status_desc'] ?? ''));
if ($statusCode <= 1) {
$statusText = 'OK';
} else {
$statusText = 'Error ' . $statusCode;
if ($statusDesc !== '') {
$statusText .= ' - ' . $statusDesc;
}
}
$runDate = $row['run_date'] ?? $row['start_date'] ?? null;
$netbackup[$key] = [
'cat' => $cat,
'row_class' => $cls,
'excluded' => false,
'ts' => strtotime((string)$runDate) ?: 0,
'vm' => $row['client'],
'last_run' => fmt_datetime($runDate),
'status' => $statusText,
'last_good' => fmt_lastgood($row['last_success']),
'size' => ($row['size_kb'] === null || $row['size_kb'] === '')
? ''
: number_format(((float)$row['size_kb']) / 1048576, 1, '.', ''),
'extra' => $row['schedule_type'],
'policy' => $row['policy'],
'message' => ($statusCode > 1) ? $statusDesc : '',
];
}
/*
Keep only the explicit NoBackup exclusions from the old VMs_Backup table.
NetBackupJobs cannot represent a deliberate exclusion because no backup job
exists for an intentionally excluded VM.
*/
$excludedRows = Invoke_Infra("
SELECT name, Exclusion, Owner, Policy
FROM VMs_Backup
WHERE Exclusion IS NOT NULL
AND LTRIM(RTRIM(Exclusion)) <> ''
");
if (is_array($excludedRows)) {
foreach ($excludedRows as $row) {
$key = normalize_host($row['name']);
// Do not overwrite a VM that has actual dun-bkp-01 job history.
if ($key === '' || isset($netbackup[$key])) continue;
$netbackup[$key] = [
'cat' => 'excluded',
'row_class' => 'table-secondary',
'excluded' => true,
'ts' => 0,
'vm' => $row['name'],
'last_run' => 'Tag NoBackup',
'status' => 'Tag NoBackup',
'last_good' => 'Tag NoBackup',
'size' => '',
'extra' => '',
'policy' => $row['Policy'],
'message' => trim((string)$row['Exclusion']),
];
}
}
/* ------------------------------------------- 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], 'Schedule', 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>
<?php ob_end_flush(); ?>