commit 71994c43bcecb73107176c0d508a7fac6fcc6366 Author: snapier Date: Tue Feb 10 18:51:31 2026 +0000 Upload files to "/" diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..2dfa15c --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,90 @@ +CHANGELOG - Storage Saturation Report Component +=============================================== + +Version 1.0.3 - January 31, 2026 +---------------------------------- +- PDF export: switched to client-side jsPDF + html2canvas (no Nagios XI Chromium/command pipeline). + Export PDF now generates the PDF in the browser and triggers download; fallback to window.print() + if jsPDF or html2canvas unavailable. Works regardless of XI command processor or Chromium. +- Progress bar: restored color styles (green/amber/red) in the minimal export document so the + "percent used" bar displays correctly when that HTML is used (e.g. client-side capture, print). +- Vendor: optional jspdf.umd.min.js in scripts/vendor/ for PDF; CDN fallback. See scripts/vendor/README.txt. + +Version 1.0.2 - January 31, 2026 +---------------------------------- +- My Reports (XI core): myreports.php parent-frame innerHTML guarded with null checks to avoid + "Cannot set properties of undefined" when parent lacks #leftnav (e.g. from component report). +- Export Image: load html2canvas from component scripts/vendor/html2canvas.min.js first, then CDN + fallback; avoids tracking prevention blocking when vendor file is present. See scripts/vendor/README.txt. +- Docs: double slash in PDF download URL (nagiosxi//reports/managereports.php) documented as known + XI core URL-building issue; fix belongs in includes/js/reports.js (not in this component). +- Export URL: strip CR/LF from Chromium report URL so stored/passed URL is one contiguous string + (avoids link being split so only first segment is clickable). XI core: fail_download() now outputs + URL in a single so the full URL is one clickable link. +- PDF/JPG export: build Chromium URL with get_localhost_url() so Chromium fetches report via + localhost; fixes ERR_ACCESS_DENIED when server cannot reach external IP. +- UI: report actions (settings, star, Export PDF, Export Image) aligned right (justify-content: + flex-end) to match Nagios XI report pattern. +- Options drawer: added neptune-drawer-options block (options-drawer-header + reportoptionpicker) + so Report Options gear opens drawer with content; minimal copy "No additional options for this report." +- Report options: added Schedule and Email links (schedulereport.php?name=... and &sendonce=1) so + full report-options row matches XI (settings, star, Schedule, Email, Export PDF, Export Image). +- Report component: full report URL passed to get_add_myreport_html() so Schedule, Email Report, + and Add to My Reports open/schedule the correct report page. +- UI: all report actions (settings, star, Schedule, Email, Export PDF, Export Image) in one + well.report-options.form-inline row; Export PDF/Image moved from header into well; alignment + matches Nagios XI report examples (e.g. Availability Summary). +- UI: removed duplicate report header (title/subtitle/date). Header shown once in page; getreport + fragment for AJAX returns only table; header included only in export (PDF) document. +- UI: numeric columns (DISK SPACE USED, DISK SPACE AVAILABLE, PERCENT USED) right-aligned to match + Nagios XI report examples (e.g. Availability Summary). +- Export/print aligned with Nagios XI built-in report pattern: + * Single entry point (index.php) with mode routing: default, mode=getreport, mode=submitpdf/submitjpg + * mode=getreport: server-side HTML report body only (for AJAX and Chromium PDF/JPG export) + * mode=submitpdf/submitjpg: uses utils-reports-export submit_report() and COMMAND_DOWNLOAD_REPORT + * Report container (#report) loaded via AJAX from index.php?mode=getreport + * PDF export uses reports.js submit_report() for async command polling and download + * reportexportlinks and get_add_myreport_html() for Add to My Reports / Schedule / Email Report +- Removed client-side window.print() and html2canvas-only PDF path in favor of XI report export +- Export Image (PNG) retained as optional client-side fallback via html2canvas + +Version 1.0.1 - January 30, 2026 +---------------------------------- +- Perfdata: support quoted labels with special characters +-- (e.g. 'C:\_Label:__Serial_Number_2cxxxxxx'=91445.6016MB;111560;132477;0;139450) +- Perfdata: when multiple metrics in one service, show the most saturated. +-- (e.g. 'C:\_Label:__Serial_Number_2cxxxxxx'=91445.6016MB;111560;132477;0;139450 'D:\_Label:__Serial_Number_2cxxxxxx'=138500.6016MB;111560;132477;0;139450) +- Caption column uses perfdata volume label when present +- UI: Neptune theme aware with fallbacks for dark/light as per Nagios Developer Standards +- UI: table class follows is_neptune() +- UI: reduced inline styles; spacing moved to CSS classes to conform with Nagios Developer Standards + +Version 1.0.0 - January 18, 2026 +---------------------------------- +- Display services with trackvolume custom variable enabled +- Filter services by trackvolume custom variable +- Parse disk usage from perfdata (check_local_disk and NCPA formats) +- Report-style display with columns: + * DISPLAY NAME (host name) + * CAPTION (service description) + * DISK SPACE USED (formatted) + * DISK SPACE AVAILABLE (formatted with color coding) + * PERCENT USED (with progress bar) +- Sort by available space (ascending - most critical first) +- Color coding for available space: + * Red (< 5 GB): Critical + * Yellow (< 50 GB): Warning + * Green (>= 50 GB): Safe +- Progress bar color coding: + * Red (>= 95%): Critical + * Orange (>= 85%): Warning + * Yellow (>= 70%): Info + * Green (< 70%): Safe +- Export functionality (PDF/JPG) using Python +- Proper Nagios XI component patterns: + * Standard initialization sequence + * Security best practices (grab_request_var, escapeshellarg) + * XML API for custom variables (template inheritance) + * Proper error handling + * CCM theme matching + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9fc9239 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (which shall not include Communications that are clearly marked or + otherwise designated in writing by the copyright owner as "Not a Work"). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is clearly marked or otherwise designated + in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed as + modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as + a result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "page" as the copyright notice for easier identification within + third-party archives. + + Copyright 2026 Everwatch.Global + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e07525 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# Storage Saturation Report for Nagios XI + +**Product:** Storage Saturation Report +**Vendor:** Everwatch.Global +**Document:** Technical documentation for implementation, calculations, and perfdata parsing. For compliance and audit traceability. +**Component version:** 1.0.2 (see CHANGELOG.txt) + +This component displays a report-style view of storage volumes that are close to capacity. Only services with the `trackvolume` custom variable enabled are included. + +--- + +## Configuration + +### Custom Variables + +To enable volume tracking for a service, add a custom variable named **`trackvolume`** with value **`1`** or **`true`** (case-insensitive) to the service configuration. + +- **`trackvolume`**: Set to `1` or `true` to include the service in the report. + +The service must provide performance data (perfdata) that includes disk usage. The component parses this data to obtain used space, total space, and derived percentage and available space. + +### Template Support + +Custom variables can be set in templates and inherit to all services using that template. + +### Navigation Path in CCM + +1. Navigate to **Core Configuration Manager** → **Hosts/Services** +2. Select the config and the service that monitors disk usage +3. Open **Custom Variables** +4. Add `trackvolume` with value `1` or `true` + +--- + +## Technical Operation + +The following flow is implemented in `saturationreport.api.php` and is deterministic for audit traceability. + +1. **Service inclusion** + Only services with custom variable **`trackvolume`** equal to **`1`** or **`true`** (case-insensitive) are included. Implemented via `get_xml_custom_service_variable_status()` and `has_trackvolume_enabled()`. + +2. **Data source** + - Service list: `get_data_service_status()` + - Perfdata: from each service’s `perfdata` field when present; if empty, from `get_xml_service_status()` (backend `getservicestatus`). + +3. **Parsing** + Each service’s single perfdata string is passed to `parse_disk_perfdata()`. Parsing tries **four formats in fixed order** (see Performance Data Parsing). The first successful parse is used. Output includes: `used`, `total`, `available`, `percent`, `used_bytes`, `total_bytes`, `available_bytes`, `used_unit`, `total_unit`, and when applicable `label`. + +4. **Multi-metric behavior** + For the single-metric format (Format 3), the perfdata string may contain multiple metrics (e.g. multiple drives). All matches are evaluated; the metric with the **highest percent used** is selected for that service. If that metric has a quoted label, it is used as the CAPTION. + +5. **Sorting** + Rows are sorted by **`disk_available_bytes` ascending** (least available first / most critical first). + +6. **Display** + CAPTION uses the perfdata **label** when present (e.g. `C:\_Label:__Serial_Number_2c3f47f4`); otherwise the service name/description. All displayed sizes and percentages are derived from the parsed values and the formulas below. + +--- + +## Calculations + +All formulas below are implemented in `saturationreport.api.php` (`parse_disk_perfdata`, `convert_to_bytes`, `format_bytes`). They are deterministic for a given perfdata string and unit handling. + +### Percent used + +- **Formula:** `percent_used = (used_bytes / total_bytes) * 100` +- **Storage/display:** Rounded to **2 decimal places** (e.g. `round($percent, 2)`). + +### Available space + +- **Formula:** `available_bytes = total_bytes - used_bytes` +- In display units: `available = total - used` (same unit as used/total). + +### Byte conversion (input → bytes) + +All capacity math uses **base 1024** for both binary and decimal-style units in this component: + +| Unit | Formula | +|------|--------| +| TiB, T | `value × 1024⁴` | +| GiB, G | `value × 1024³` | +| MiB, M | `value × 1024²` | +| KiB, K | `value × 1024` | +| B, (none), % | `value` (no scaling) | + +- Implemented in: `convert_to_bytes($value, $unit)`. + +### Human-readable display (bytes → display) + +- **Function:** `format_bytes($bytes, $unit)` +- Divides bytes by 1024^n and rounds to **1 decimal place**. +- Unit selection: TB if bytes ≥ 1024⁴, else GB if ≥ 1024³, else MB if ≥ 1024², else KB if ≥ 1024, else B. + +### Unit inference + +- If **total** (or max) has no unit but **used** has a unit, total is interpreted in the **same unit as used** (e.g. both MB). + +--- + +## Performance Data (Perfdata) Parsing + +Parsing is attempted in the following order. The **first** successful format wins. All patterns are PCRE and case-insensitive unless noted. + +### Format 1 – NCPA-style quoted `used` / `total` + +- Two metrics: `'used'=value[unit];` and `'total'=value[unit];` +- **Patterns:** + - Used: `/'used'\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i` + - Total: `/'total'\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i` +- **Capture groups:** (1) numeric value, (2) unit (e.g. MB, MiB) +- **Example:** `'used'=0.69GiB;;; 'total'=5.82GiB;;;` + +### Format 2 – NCPA-style unquoted `used` / `total` + +- Same as Format 1 with unquoted labels. +- **Patterns:** + - Used: `/\bused\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i` + - Total: `/\btotal\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i` +- **Example:** `used=0.69GiB;;; total=5.82GiB;;;` + +### Format 3 – Single-metric with optional quoted label (multi-metric) + +- Standard Nagios: **label**`=`**value**`[unit]`**;**warn**;**crit**;**min**;**max**`[unit]`. Value is “used”; 5th field is “max” (total). If max has no unit, the value’s unit is used. +- **Label:** Quoted `'...'` (any characters except single quote) or unquoted (non-whitespace, non-`=`). +- **Full pattern:** + `(?:'([^']*)'|[^=\s]+)=([0-9.]+)([KMGT]?(?:IB|B)?);[^;]*;[^;]*;[^;]*;([0-9.]+)([KMGT]?(?:IB|B)?)?` +- **Capture groups:** (1) label if quoted, (2) value, (3) value unit, (4) max/total, (5) max unit if present. +- **Example:** `'C:\_Label:__Serial_Number_2c3f47f4'=91445.6016MB;111560;132477;0;139450` +- When **multiple** such metrics appear in one perfdata string, the metric with the **largest percent used** is chosen; its label (if any) is used for CAPTION. + +### Format 4 – Fallback single-metric (no label capture) + +- Same structure as Format 3; label is not captured. +- **Pattern:** `/[^=]*=([0-9.]+)([KMGT]?(?:IB|B)?);[^;]*;[^;]*;[^;]*;([0-9.]+)([KMGT]?(?:IB|B)?)?/i` +- **Capture groups:** (1) value, (2) value unit, (3) max, (4) max unit. +- **Example:** `/=4985MiB;9592;10791;0;11991` + +### Unit token + +- **Pattern:** `[KMGT]?(?:IB|B)?` — optional K/M/G/T followed by optional `iB` or `B` (e.g. M, MB, MiB). + +--- + +## Display + +The report is a table, ordered by available disk space (ascending). Each row shows: + +- **DISPLAY NAME:** Host name +- **CAPTION:** Perfdata volume label when present (e.g. `C:\_Label:__Serial_Number_2c3f47f4`), otherwise service description/name +- **DISK SPACE USED:** Used space (e.g. "117 GB") +- **DISK SPACE AVAILABLE:** Available space (e.g. "2.7 GB") with color coding +- **PERCENT USED:** Percentage with progress bar (e.g. "97%") + +--- + +## Export and Print + +Export uses **client-side** PDF and image generation (jsPDF + html2canvas). No dependency on Nagios XI Chromium or the report command pipeline. + +- **PDF:** Client-side export: html2canvas captures `#saturationreport-container`, jsPDF creates a one-page PDF and triggers download. Libraries loaded from `scripts/vendor/` when present (html2canvas.min.js, jspdf.umd.min.js), else CDN. Fallback to browser Print (e.g. Print to PDF) if jsPDF or html2canvas unavailable. +- **Image (PNG):** Client-side: html2canvas captures `#saturationreport-container` (2× scale). Same vendor/CDN loading as PDF. +- **Progress bar:** The "percent used" bar uses color classes (green/amber/red) in both the main stylesheet and the minimal export document so the bar is styled correctly in the report and in exported PDF/image. + +Add to My Reports / Schedule / Email use `get_add_myreport_html()` with the full report URL. All report actions (settings, star, Schedule, Email, Export PDF, Export Image) are in the report options well in a single row. + +### Mode routing (index.php) + +- **default:** Full page with form, reportexportlinks, and `#report` container. Report body is loaded via AJAX from `?mode=getreport`. +- **mode=getreport:** Returns only the report HTML (table and optional header). Used by (1) AJAX to fill `#report`, (2) client-side capture for PDF/image. +- **mode=submitpdf** / **mode=submitjpg:** Still available for XI installs that use the Chromium pipeline; the Export PDF button uses client-side jsPDF + html2canvas by default. + +--- + +## Compliance and Audit + +- **Implementation reference:** Parsing and calculations are in `saturationreport.api.php`: `parse_disk_perfdata()`, `convert_to_bytes()`, `format_bytes()`, `get_saturationreport_services_data()`, `has_trackvolume_enabled()`. +- **Parsing order:** Formats 1 → 2 → 3 → 4; first successful parse is used. +- **Determinism:** For a given perfdata string and unit rules, percent used and available space are uniquely determined by the formulas and pattern order above. +- **Version:** This document and the formulas/patterns apply to component version 1.0.3 (see CHANGELOG.txt). + +--- + +## Author / Vendor + +**Everwatch.Global** +https://everwatch.global + +Component development: snapier +Technical documentation: Component version 1.0.3, January 2026 diff --git a/index.php b/index.php new file mode 100644 index 0000000..a0466a8 --- /dev/null +++ b/index.php @@ -0,0 +1,272 @@ + $v) { + $query[$k] = $v; + } + $query['token'] = user_generate_auth_token(get_user_id($_SESSION['username'])); + $query['locale'] = isset($_SESSION['language']) ? $_SESSION['language'] : 'en_US'; + $query['records'] = 100000; + $query['mode'] = 'getreport'; + $query['hideoptions'] = 1; + $query['export'] = 1; + $base_url = get_localhost_url(); + $path = (substr($base_url, -1) === '/') ? 'includes/components/saturationreport/index.php' : '/includes/components/saturationreport/index.php'; + $url = $base_url . $path . '?' . http_build_query($query); + $url = str_replace(["\r", "\n"], '', trim($url)); + $filename = 'index-' . uniqid() . '.' . $type; + $args = array( + 'filename' => $filename, + 'url' => $url, + 'type' => $type, + 'orientation' => 0, + ); + $command_id = submit_command(COMMAND_DOWNLOAD_REPORT, serialize($args)); + header('Content-Type: application/json'); + echo json_encode(array('command_id' => $command_id)); + exit; +} + +// Normal display +display_saturationreport(); + +/** + * Output report HTML only (no full page wrapper). + * Used by: (1) AJAX load into #report, (2) Chromium for PDF/JPG export. + */ +function get_saturationreport_report() +{ + define('SATURATIONREPORT_DATA_ONLY', 1); + require_once(dirname(__FILE__) . '/saturationreport.api.php'); + + $services = get_saturationreport_services_data(); + if (isset($services['error'])) { + echo '
' . encode_form_val($services['error']) . '
'; + return; + } + if (!is_array($services)) { + $services = array(); + } + + $table_class = (function_exists('is_neptune') && is_neptune()) + ? 'table table-condensed table-striped table-bordered tablesorter' + : 'table table-striped table-bordered'; + + $hideoptions = grab_request_var('hideoptions', 0); + $export = grab_request_var('export', 0); + $is_export = (int)$hideoptions === 1 || (int)$export === 1; + + if ($is_export) { + // Minimal HTML document for Chromium print-to-PDF (and client-side capture) + header('Content-Type: text/html; charset=utf-8'); + echo '' . _('Storage Saturation Report') . ''; + echo ''; + } + + echo '
'; + if ($is_export) { + echo '
'; + echo '

' . _('Storage Saturation Report') . '

'; + echo '

' . _('Summary of Tracked Storage: Volumes') . '

'; + echo '

' . _('Report Date:') . ' ' . date('n/j/y, g:i A') . '

'; + echo '
'; + } + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if (count($services) === 0) { + echo ''; + } else { + foreach ($services as $s) { + $host_name = isset($s['host_name']) ? $s['host_name'] : 'N/A'; + $caption = isset($s['caption_display']) ? $s['caption_display'] : (isset($s['service_description']) ? $s['service_description'] : 'N/A'); + $disk_used = isset($s['disk_used_display']) ? $s['disk_used_display'] : 'N/A'; + $disk_available = isset($s['disk_available_display']) ? $s['disk_available_display'] : 'N/A'; + $percent = isset($s['disk_usage_percent']) ? $s['disk_usage_percent'] : 0; + $available_bytes = isset($s['disk_available_bytes']) ? (int)$s['disk_available_bytes'] : 0; + + $available_extra = ''; + if ($available_bytes > 0) { + $available_gb = $available_bytes / (1024 * 1024 * 1024); + if ($available_gb < 5) { + $available_extra = ' text-danger'; + } elseif ($available_gb < 50) { + $available_extra = ' text-warning'; + } + } + + $percent_class = 'progress-bar-success'; + if ($percent >= 95) { + $percent_class = 'progress-bar-danger'; + } elseif ($percent >= 85) { + $percent_class = 'progress-bar-warning'; + } elseif ($percent >= 70) { + $percent_class = 'progress-bar-info'; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + } + + echo '
' . _('DISPLAY NAME') . '' . _('CAPTION') . '' . _('DISK SPACE USED') . '' . _('DISK SPACE AVAILABLE') . '' . _('PERCENT USED') . '
' . _('No services with trackvolume custom variable found') . '
' . encode_form_val($host_name) . '' . encode_form_val($caption) . '' . encode_form_val($disk_used) . '' . encode_form_val($disk_available) . ''; + if ($percent > 0) { + echo '
' . number_format($percent, 1) . '%
'; + } else { + echo 'N/A'; + } + echo '
'; + + if ($is_export) { + echo ''; + } +} + +/** + * Display the storage saturation report (full page with form and report container) + */ +function display_saturationreport() +{ + $component_url = get_component_url_base('saturationreport'); + $table_class = (function_exists('is_neptune') && is_neptune()) + ? 'table table-condensed table-striped table-bordered tablesorter' + : 'table table-striped table-bordered'; + + do_page_start(array('page_title' => _('Storage Saturation Report')), true); +?> + + + + +
+ +
+
+ + + +
+
+

+ close +
+
+

+
+
+
+
+ +
+

+

+

+
+ +
+
+ + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + + + + + +customvar)) { + return false; + } + + // Iterate through custom variables to find trackvolume + foreach ($customvars->customvar as $customvar) { + $name = (string)$customvar->name; + $value = (string)$customvar->value; + + if ($name === 'trackvolume' || $name === 'TRACKVOLUME') { + // Check if value is 1, "1", "true", or "TRUE" + if ($value === '1' || $value === 1 || strtolower($value) === 'true') { + return true; + } + } + } + + return false; +} + +/** + * Parse perfdata to extract disk usage information + * Supports multiple formats: + * 1. check_local_disk format: /=4985MiB;9592;10791;0;11991 + * 2. NCPA format: 'used'=0.69GiB;;; 'free'=4.81GiB;;; 'total'=5.82GiB;;; + * + * @param string $perfdata Performance data string from service status + * @param string $check_command Optional check command name to help identify format + * @return array|false Array with 'used', 'total', 'percent', 'available' or false if not found + */ +function parse_disk_perfdata($perfdata, $check_command = '') +{ + if (empty($perfdata)) { + return false; + } + + $used_value = null; + $used_unit = ''; + $total_value = null; + $total_unit = ''; + + // Pattern to match 'used'=value[unit];;; (with quotes) + if (preg_match("/'used'\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i", $perfdata, $used_matches)) { + $used_value = floatval($used_matches[1]); + $used_unit = isset($used_matches[2]) ? strtoupper(trim($used_matches[2])) : ''; + } + // Pattern to match 'total'=value[unit];;; (with quotes) + if (preg_match("/'total'\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i", $perfdata, $total_matches)) { + $total_value = floatval($total_matches[1]); + $total_unit = isset($total_matches[2]) ? strtoupper(trim($total_matches[2])) : ''; + } + + // If we found both used and total, calculate percentage + if ($used_value !== null && $total_value !== null && $total_value > 0) { + // If total has no unit but used has a unit, assume total uses same unit + if (empty($total_unit) && !empty($used_unit) && $total_value > 0) { + $total_unit = $used_unit; + } + + // Convert to bytes for calculation + $used_bytes = convert_to_bytes($used_value, $used_unit); + $total_bytes = convert_to_bytes($total_value, $total_unit); + + // Calculate percentage + if ($total_bytes > 0 && $used_bytes > 0) { + $percent = ($used_bytes / $total_bytes) * 100; + $available_bytes = $total_bytes - $used_bytes; + return array( + 'used' => $used_value, + 'total' => $total_value, + 'available' => $total_value - $used_value, + 'percent' => round($percent, 2), + 'used_bytes' => $used_bytes, + 'total_bytes' => $total_bytes, + 'available_bytes' => $available_bytes, + 'used_unit' => $used_unit, + 'total_unit' => $total_unit, + 'label' => '' + ); + } + } + + // Try alternative NCPA format without quotes: used=0.69GiB;;; free=4.81GiB;;; total=5.82GiB;;; + if ($used_value === null || $total_value === null) { + $used_value = null; + $used_unit = ''; + $total_value = null; + $total_unit = ''; + + if (preg_match("/\bused\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i", $perfdata, $used_matches)) { + $used_value = floatval($used_matches[1]); + $used_unit = isset($used_matches[2]) ? strtoupper(trim($used_matches[2])) : ''; + } + if (preg_match("/\btotal\s*=\s*([0-9.]+)([KMGT]?(?:IB|B)?);/i", $perfdata, $total_matches)) { + $total_value = floatval($total_matches[1]); + $total_unit = isset($total_matches[2]) ? strtoupper(trim($total_matches[2])) : ''; + } + + if ($used_value !== null && $total_value !== null && $total_value > 0) { + if (empty($total_unit) && !empty($used_unit) && $total_value > 0) { + $total_unit = $used_unit; + } + + $used_bytes = convert_to_bytes($used_value, $used_unit); + $total_bytes = convert_to_bytes($total_value, $total_unit); + + if ($total_bytes > 0 && $used_bytes > 0) { + $percent = ($used_bytes / $total_bytes) * 100; + $available_bytes = $total_bytes - $used_bytes; + return array( + 'used' => $used_value, + 'total' => $total_value, + 'available' => $total_value - $used_value, + 'percent' => round($percent, 2), + 'used_bytes' => $used_bytes, + 'total_bytes' => $total_bytes, + 'available_bytes' => $available_bytes, + 'used_unit' => $used_unit, + 'total_unit' => $total_unit, + 'label' => '' + ); + } + } + } + + // Try check_local_disk / single-metric format: 'label'=value[unit];warn;crit;min;max + // Supports quoted labels with special chars (e.g. 'C:\_Label:__Serial_Number_2c3f47f4'=91445.6016MB;111560;132477;0;139450) + // If multiple metrics present, pick the one with highest percent used (most saturated) + $label_pattern = "(?:'([^']*)'|[^=\s]+)"; + $value_pattern = "=([0-9.]+)([KMGT]?(?:IB|B)?);[^;]*;[^;]*;[^;]*;([0-9.]+)([KMGT]?(?:IB|B)?)?"; + $full_pattern = '/' . $label_pattern . $value_pattern . '/i'; + if (preg_match_all($full_pattern, $perfdata, $all_matches, PREG_SET_ORDER)) { + $best = null; + $best_percent = -1.0; + foreach ($all_matches as $m) { + $label = isset($m[1]) ? trim($m[1]) : ''; + $used = floatval($m[2]); + $used_unit = isset($m[3]) ? strtoupper(trim($m[3])) : ''; + $total = isset($m[4]) ? floatval($m[4]) : 0; + $total_unit = isset($m[5]) ? strtoupper(trim($m[5])) : ''; + if (empty($total_unit) && !empty($used_unit) && $total > 0) { + $total_unit = $used_unit; + } + $used_bytes = convert_to_bytes($used, $used_unit); + $total_bytes = $total > 0 ? convert_to_bytes($total, $total_unit) : 0; + if ($total_bytes <= 0 || $used_bytes <= 0) { + continue; + } + $percent = ($used_bytes / $total_bytes) * 100; + if ($percent > $best_percent) { + $best_percent = $percent; + $available_bytes = $total_bytes - $used_bytes; + $best = array( + 'used' => $used, + 'total' => $total, + 'available' => $total - $used, + 'percent' => round($percent, 2), + 'used_bytes' => $used_bytes, + 'total_bytes' => $total_bytes, + 'available_bytes' => $available_bytes, + 'used_unit' => $used_unit, + 'total_unit' => $total_unit, + 'label' => $label + ); + } + } + if ($best !== null) { + return $best; + } + } + + // Fallback: single match without label (original pattern) /=4985MiB;9592;10791;0;11991 + // Pattern: Handle binary units (KiB, MiB, GiB, TiB) and decimal units (KB, MB, GB, TB) + // Matches: label=value[unit];warn;crit;min;max[unit] + $pattern = '/[^=]*=([0-9.]+)([KMGT]?(?:IB|B)?);[^;]*;[^;]*;[^;]*;([0-9.]+)([KMGT]?(?:IB|B)?)?/i'; + + if (preg_match($pattern, $perfdata, $matches)) { + $used = floatval($matches[1]); + $used_unit = isset($matches[2]) ? strtoupper(trim($matches[2])) : ''; + $total = isset($matches[3]) ? floatval($matches[3]) : 0; + $total_unit = isset($matches[4]) ? strtoupper(trim($matches[4])) : ''; + + // If total has no unit but used has a unit, assume total uses same unit + if (empty($total_unit) && !empty($used_unit) && $total > 0) { + $total_unit = $used_unit; + } + + // Convert to bytes for calculation if units are present + $used_bytes = convert_to_bytes($used, $used_unit); + $total_bytes = $total > 0 ? convert_to_bytes($total, $total_unit) : 0; + + // Calculate percentage if we have both used and total + if ($total_bytes > 0 && $used_bytes > 0) { + $percent = ($used_bytes / $total_bytes) * 100; + $available_bytes = $total_bytes - $used_bytes; + return array( + 'used' => $used, + 'total' => $total, + 'available' => $total - $used, + 'percent' => round($percent, 2), + 'used_bytes' => $used_bytes, + 'total_bytes' => $total_bytes, + 'available_bytes' => $available_bytes, + 'used_unit' => $used_unit, + 'total_unit' => $total_unit, + 'label' => '' + ); + } + + // If we have total but couldn't convert (no units), try direct calculation + if ($total > 0 && $used > 0 && empty($used_unit) && empty($total_unit)) { + $percent = ($used / $total) * 100; + $available = $total - $used; + return array( + 'used' => $used, + 'total' => $total, + 'available' => $available, + 'percent' => round($percent, 2), + 'used_bytes' => 0, + 'total_bytes' => 0, + 'available_bytes' => 0, + 'used_unit' => '', + 'total_unit' => '', + 'label' => '' + ); + } + } + + return false; +} + +/** + * Convert value with unit to bytes + * Supports both binary (KiB, MiB, GiB, TiB) and decimal (KB, MB, GB, TB) units + * + * @param float $value Numeric value + * @param string $unit Unit (B, KB, MB, GB, TB, KiB, MiB, GiB, TiB, K, M, G, T, %) + * @return float Value in bytes (or original if percentage or no unit) + */ +function convert_to_bytes($value, $unit) +{ + $unit = strtoupper(trim($unit)); + + // Handle binary units (KiB, MiB, GiB, TiB) - base 1024 + if (strpos($unit, 'IB') !== false || strpos($unit, 'I') !== false) { + switch ($unit) { + case 'TIB': + case 'TI': + return $value * 1024 * 1024 * 1024 * 1024; + case 'GIB': + case 'GI': + return $value * 1024 * 1024 * 1024; + case 'MIB': + case 'MI': + return $value * 1024 * 1024; + case 'KIB': + case 'KI': + return $value * 1024; + default: + return $value; + } + } + + // Handle decimal units (KB, MB, GB, TB) or single letter (K, M, G, T) + switch ($unit) { + case 'TB': + case 'T': + return $value * 1024 * 1024 * 1024 * 1024; + case 'GB': + case 'G': + return $value * 1024 * 1024 * 1024; + case 'MB': + case 'M': + return $value * 1024 * 1024; + case 'KB': + case 'K': + return $value * 1024; + case 'B': + case '%': + case '': + default: + return $value; + } +} + +/** + * Format bytes to human-readable format + * + * @param float $bytes Number of bytes + * @param string $unit Preferred unit (GB, MB, etc.) or empty for auto + * @return array Array with 'value' and 'unit' + */ +function format_bytes($bytes, $unit = '') +{ + if ($bytes == 0) { + return array('value' => 0, 'unit' => 'GB'); + } + + // If unit is specified, use it + if (!empty($unit)) { + $unit = strtoupper($unit); + switch ($unit) { + case 'TB': + return array('value' => round($bytes / (1024 * 1024 * 1024 * 1024), 1), 'unit' => 'TB'); + case 'GB': + return array('value' => round($bytes / (1024 * 1024 * 1024), 1), 'unit' => 'GB'); + case 'MB': + return array('value' => round($bytes / (1024 * 1024), 1), 'unit' => 'MB'); + case 'KB': + return array('value' => round($bytes / 1024, 1), 'unit' => 'KB'); + default: + break; + } + } + + // Auto-format based on size + if ($bytes >= 1024 * 1024 * 1024 * 1024) { + return array('value' => round($bytes / (1024 * 1024 * 1024 * 1024), 1), 'unit' => 'TB'); + } elseif ($bytes >= 1024 * 1024 * 1024) { + return array('value' => round($bytes / (1024 * 1024 * 1024), 1), 'unit' => 'GB'); + } elseif ($bytes >= 1024 * 1024) { + return array('value' => round($bytes / (1024 * 1024), 1), 'unit' => 'MB'); + } elseif ($bytes >= 1024) { + return array('value' => round($bytes / 1024, 1), 'unit' => 'KB'); + } else { + return array('value' => round($bytes, 1), 'unit' => 'B'); + } +} + +/** + * Route the request call from script main.js + */ +function route_request() +{ + // Set JSON header early, before any output + if (!headers_sent()) { + header('Content-Type: application/json'); + } + + // grab request values + $mode = grab_request_var('mode'); + + switch ( $mode ) { + + case 'services': + generate_saturationreport_services(); + break; + + default: + echo json_encode(array('error' => 'Invalid mode')); + break; + } +} + +/** + * Get storage saturation report service data + * Filters services with trackvolume custom variable = 1 or "true" + * Returns array of service data + */ +function get_saturationreport_services_data() +{ + $services = array(); + + // Check if required functions are available + if (!function_exists('get_data_service_status')) { + return array('error' => 'get_data_service_status() function not available'); + } + + if (!function_exists('get_xml_custom_service_variable_status')) { + return array('error' => 'get_xml_custom_service_variable_status() function not available'); + } + + try { + // Get all services + $request_args = array(); + $service_objects = get_data_service_status($request_args); + + if (!is_array($service_objects)) { + return array('error' => 'Service objects is not an array'); + } + + // Filter services by trackvolume custom variable + foreach ($service_objects as $obj) { + if (!is_array($obj)) { + continue; + } + + $host_name = $obj['host_name']; + $service_description = $obj['service_description']; + + // Get custom variables + $args = array( + "host_name" => $host_name, + "service_description" => $service_description + ); + $xml_result = get_xml_custom_service_variable_status($args); + + // Check if trackvolume is enabled + $has_trackvolume = false; + if ($xml_result && isset($xml_result->customservicevarstatus->customvars)) { + $has_trackvolume = has_trackvolume_enabled($xml_result->customservicevarstatus->customvars); + } + + if (!$has_trackvolume) { + continue; + } + + // Get service ID and name + $id = intval($obj['service_object_id']); + $name = isset($obj['display_name']) && !empty($obj['display_name']) + ? $obj['display_name'] + : $service_description; + + // Get perfdata - try direct from get_data_service_status() first (like diskpressure) + $perfdata = isset($obj['perfdata']) ? $obj['perfdata'] : ''; + + // If not available, get it separately + if (empty($perfdata) && function_exists('get_xml_service_status')) { + $backendargs = array( + "cmd" => "getservicestatus", + "host_name" => "=" . $host_name, + "service_description" => "=" . $service_description + ); + $xml_status = get_xml_service_status($backendargs); + if ($xml_status && isset($xml_status->servicestatus)) { + $perfdata = strval($xml_status->servicestatus->performance_data); + } + } + + // Parse disk usage from perfdata + $disk_info = parse_disk_perfdata($perfdata); + + // Build service data + $service_data = array( + 'service_id' => $id, + 'name' => $name, + 'host_name' => $host_name, + 'service_description' => $service_description, + 'perfdata' => $perfdata + ); + + // Add disk usage information if available + if ($disk_info !== false) { + $service_data['disk_usage_percent'] = $disk_info['percent']; + $service_data['disk_used'] = $disk_info['used']; + $service_data['disk_total'] = $disk_info['total']; + $service_data['disk_available'] = $disk_info['available']; + $service_data['disk_used_bytes'] = $disk_info['used_bytes']; + $service_data['disk_total_bytes'] = $disk_info['total_bytes']; + $service_data['disk_available_bytes'] = $disk_info['available_bytes']; + $service_data['disk_used_unit'] = $disk_info['used_unit']; + $service_data['disk_total_unit'] = $disk_info['total_unit']; + $service_data['disk_label'] = isset($disk_info['label']) ? $disk_info['label'] : ''; + + // Caption: use perfdata volume label when present (e.g. C:\_Label:__Serial_Number_...), else service description + if (!empty($service_data['disk_label'])) { + $service_data['caption_display'] = $service_data['disk_label']; + } else { + $service_data['caption_display'] = $service_data['name']; + } + + // Format values for display + $used_formatted = format_bytes($service_data['disk_used_bytes'], $service_data['disk_used_unit']); + $available_formatted = format_bytes($service_data['disk_available_bytes'], $service_data['disk_total_unit']); + + $service_data['disk_used_display'] = $used_formatted['value'] . ' ' . $used_formatted['unit']; + $service_data['disk_available_display'] = $available_formatted['value'] . ' ' . $available_formatted['unit']; + $service_data['disk_percent_display'] = number_format($disk_info['percent'], 0) . '%'; + } else { + // Set defaults if perfdata can't be parsed + $service_data['disk_usage_percent'] = 0; + $service_data['disk_used'] = 0; + $service_data['disk_total'] = 0; + $service_data['disk_available'] = 0; + $service_data['disk_used_bytes'] = 0; + $service_data['disk_total_bytes'] = 0; + $service_data['disk_available_bytes'] = 0; + $service_data['disk_label'] = ''; + $service_data['caption_display'] = $service_data['name']; + $service_data['disk_used_display'] = 'N/A'; + $service_data['disk_available_display'] = 'N/A'; + $service_data['disk_percent_display'] = 'N/A'; + } + + $services[$id] = $service_data; + } + + // Sort by available space (ascending - most critical first) + if (count($services) > 0) { + usort($services, function($a, $b) { + $available_a = isset($a['disk_available_bytes']) ? $a['disk_available_bytes'] : PHP_INT_MAX; + $available_b = isset($b['disk_available_bytes']) ? $b['disk_available_bytes'] : PHP_INT_MAX; + return $available_a <=> $available_b; + }); + + // Re-index array + $services = array_values($services); + } + + } catch (Exception $e) { + return array('error' => 'Exception: ' . $e->getMessage()); + } catch (Error $e) { + return array('error' => 'Fatal error: ' . $e->getMessage()); + } + + // Return empty array if no services found (not an error) + return $services; +} + +/** + * Generate JSON output for services + */ +function generate_saturationreport_services() +{ + $services = get_saturationreport_services_data(); + echo json_encode($services); +} +