Upload files to "/"
This commit is contained in:
90
CHANGELOG.txt
Normal file
90
CHANGELOG.txt
Normal file
@@ -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 <a href="..."> 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
|
||||
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -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.
|
||||
189
README.md
Normal file
189
README.md
Normal file
@@ -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
|
||||
272
index.php
Normal file
272
index.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
//
|
||||
// Storage Saturation Report Component - Main Display Page
|
||||
//
|
||||
|
||||
require_once(dirname(__FILE__).'/../../../config.inc.php');
|
||||
require_once(dirname(__FILE__) . '/../../common.inc.php');
|
||||
|
||||
// Initialization
|
||||
pre_init();
|
||||
init_session();
|
||||
grab_request_vars();
|
||||
check_prereqs();
|
||||
check_authentication(false);
|
||||
|
||||
$mode = grab_request_var('mode');
|
||||
|
||||
// Return report body only (for AJAX inject and for Chromium PDF export)
|
||||
if ($mode === 'getreport') {
|
||||
get_saturationreport_report();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Submit PDF/JPG command and return JSON { command_id } for reports.js polling.
|
||||
// Use get_localhost_url() so Chromium fetches the report via localhost (fixes ERR_ACCESS_DENIED when server cannot reach external IP).
|
||||
if ($mode === 'submitpdf' || $mode === 'submitjpg') {
|
||||
if (!function_exists('submit_command') || !function_exists('user_generate_auth_token')) {
|
||||
require_once(dirname(__FILE__) . '/../../utils-reports-export.inc.php');
|
||||
}
|
||||
$type = ($mode === 'submitjpg') ? 'jpg' : 'pdf';
|
||||
$query = array();
|
||||
foreach ($_GET as $k => $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 '<div class="alert alert-danger">' . encode_form_val($services['error']) . '</div>';
|
||||
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 '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' . _('Storage Saturation Report') . '</title>';
|
||||
echo '<style>';
|
||||
echo 'body{font-family:inherit;padding:12px;} .text-right{text-align:right;}';
|
||||
echo '.progress{height:20px;margin:0;background:#e9ecef;border-radius:4px;overflow:hidden;position:relative;}';
|
||||
echo '.progress-bar{float:left;height:100%;font-size:12px;line-height:20px;text-align:center;font-weight:bold;padding:0 5px;box-sizing:border-box;}';
|
||||
echo '.progress-bar-success{background:#28a745;color:#fff;}';
|
||||
echo '.progress-bar-info{background:#fd7e14;color:#212529;}';
|
||||
echo '.progress-bar-warning{background:#f0ad4e;color:#fff;}';
|
||||
echo '.progress-bar-danger{background:#dc3545;color:#fff;}';
|
||||
echo '</style></head><body>';
|
||||
}
|
||||
|
||||
echo '<div id="saturationreport-container" class="saturationreport-report">';
|
||||
if ($is_export) {
|
||||
echo '<div class="report-header">';
|
||||
echo '<h1>' . _('Storage Saturation Report') . '</h1>';
|
||||
echo '<p class="subtitle">' . _('Summary of Tracked Storage: Volumes') . '</p>';
|
||||
echo '<p class="report-date">' . _('Report Date:') . ' ' . date('n/j/y, g:i A') . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '<table class="' . encode_form_val($table_class) . '" id="saturationreport-table">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . _('DISPLAY NAME') . '</th>';
|
||||
echo '<th>' . _('CAPTION') . '</th>';
|
||||
echo '<th class="text-right">' . _('DISK SPACE USED') . '</th>';
|
||||
echo '<th class="text-right">' . _('DISK SPACE AVAILABLE') . '</th>';
|
||||
echo '<th class="text-right">' . _('PERCENT USED') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (count($services) === 0) {
|
||||
echo '<tr><td colspan="5" class="text-center">' . _('No services with trackvolume custom variable found') . '</td></tr>';
|
||||
} 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 '<tr>';
|
||||
echo '<td>' . encode_form_val($host_name) . '</td>';
|
||||
echo '<td>' . encode_form_val($caption) . '</td>';
|
||||
echo '<td class="text-right">' . encode_form_val($disk_used) . '</td>';
|
||||
echo '<td class="text-right' . $available_extra . '">' . encode_form_val($disk_available) . '</td>';
|
||||
echo '<td class="text-right">';
|
||||
if ($percent > 0) {
|
||||
echo '<div class="progress"><div class="progress-bar ' . $percent_class . '" style="width:' . (float)$percent . '%" role="progressbar" aria-valuenow="' . (float)$percent . '" aria-valuemin="0" aria-valuemax="100">' . number_format($percent, 1) . '%</div></div>';
|
||||
} else {
|
||||
echo '<span class="text-muted">N/A</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</tbody></table></div>';
|
||||
|
||||
if ($is_export) {
|
||||
echo '</body></html>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
?>
|
||||
<head>
|
||||
<link rel="stylesheet" href="<?php echo $component_url; ?>/styles/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid saturationreport-page">
|
||||
<?php
|
||||
$report_url = get_base_url();
|
||||
if (substr($report_url, -1) !== '/') {
|
||||
$report_url .= '/';
|
||||
}
|
||||
$report_url .= 'includes/components/saturationreport/index.php';
|
||||
?>
|
||||
<form method="get" id="saturationreport-form" data-type="saturationreport">
|
||||
<div class="well report-options form-inline">
|
||||
<div class="reportexportlinks">
|
||||
<?php
|
||||
if (function_exists('get_add_myreport_html')) {
|
||||
echo get_add_myreport_html(_('Storage Saturation Report'), $report_url, array());
|
||||
}
|
||||
$base = get_base_url();
|
||||
if (substr($base, -1) !== '/') {
|
||||
$base .= '/';
|
||||
}
|
||||
$schedule_url = $base . 'includes/components/scheduledreporting/schedulereport.php?name=' . urlencode('Storage Saturation Report');
|
||||
$email_url = $schedule_url . '&sendonce=1';
|
||||
?>
|
||||
<a href="<?php echo encode_form_val($schedule_url); ?>" data-url="<?php echo encode_form_val($schedule_url); ?>" alt="<?php echo _('Schedule this Report'); ?>" title="<?php echo _('Schedule this Report'); ?>" class="btn btn-sm btn-default tt-bind btn-report-action icon-in-btn" data-placement="bottom"><i class="material-symbols-outlined md-middle md-400">schedule</i></a>
|
||||
<a href="<?php echo encode_form_val($email_url); ?>" data-url="<?php echo encode_form_val($email_url); ?>" alt="<?php echo _('Email this Report'); ?>" title="<?php echo _('Email this Report'); ?>" class="btn btn-sm btn-default tt-bind btn-report-action icon-in-btn" data-placement="bottom"><i class="material-symbols-outlined md-middle md-400 md-fill">mail</i></a>
|
||||
</div>
|
||||
<button type="button" id="export-pdf-btn" class="btn btn-sm btn-primary btn-export" data-type="submitpdf">
|
||||
<i class="fa fa-file-pdf-o"></i> <?php echo _('Export PDF'); ?>
|
||||
</button>
|
||||
<button type="button" id="export-image-btn" class="btn btn-sm btn-info">
|
||||
<i class="fa fa-file-image-o"></i> <?php echo _('Export Image'); ?>
|
||||
</button>
|
||||
<div class="neptune-drawer-options">
|
||||
<div class="options-drawer-header">
|
||||
<h4><?php echo _('Options'); ?></h4>
|
||||
<i id="close-report-options" class="material-symbols-outlined md-20 md-400 md-button md-action">close</i>
|
||||
</div>
|
||||
<div class="reportoptionpicker">
|
||||
<p class="text-muted"><?php echo _('No additional options for this report.'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="report-header">
|
||||
<h1><?php echo _('Storage Saturation Report'); ?></h1>
|
||||
<p class="subtitle"><?php echo _('Summary of Tracked Storage: Volumes'); ?></p>
|
||||
<p class="report-date"><?php echo _('Report Date:'); ?> <?php echo date('n/j/y, g:i A'); ?></p>
|
||||
</div>
|
||||
|
||||
<div id="report">
|
||||
<div id="saturationreport-container">
|
||||
<table class="<?php echo encode_form_val($table_class); ?>" id="saturationreport-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php echo _('DISPLAY NAME'); ?></th>
|
||||
<th><?php echo _('CAPTION'); ?></th>
|
||||
<th class="text-right"><?php echo _('DISK SPACE USED'); ?></th>
|
||||
<th class="text-right"><?php echo _('DISK SPACE AVAILABLE'); ?></th>
|
||||
<th class="text-right"><?php echo _('PERCENT USED'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="saturationreport-services">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i> <?php echo _('Loading...'); ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.saturationreportApiUrl = '<?php echo $component_url; ?>/saturationreport.api.php';
|
||||
window.saturationreportBaseUrl = '<?php echo get_base_url(); ?>includes/components/saturationreport/index.php';
|
||||
</script>
|
||||
<?php if (function_exists('get_build_id')) { ?>
|
||||
<script src="<?php echo get_base_url(); ?>includes/js/reports.js?<?php echo get_build_id(); ?>"></script>
|
||||
<?php } ?>
|
||||
<script src="<?php echo $component_url; ?>/scripts/main.js"></script>
|
||||
</body>
|
||||
<?php
|
||||
do_page_end(true);
|
||||
}
|
||||
545
saturationreport.api.php
Normal file
545
saturationreport.api.php
Normal file
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
//
|
||||
// Storage Saturation Report Component - API Endpoint
|
||||
//
|
||||
|
||||
require_once(dirname(__FILE__) . '/../componenthelper.inc.php');
|
||||
|
||||
// Only initialize and route if not being called for data only (from index.php export mode)
|
||||
if (!defined('SATURATIONREPORT_DATA_ONLY')) {
|
||||
// Normal API mode - initialize and route
|
||||
pre_init();
|
||||
init_session();
|
||||
grab_request_vars();
|
||||
check_prereqs();
|
||||
check_authentication();
|
||||
route_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service has trackvolume custom variable set to 1 or "true"
|
||||
* Based on customvariabletab component pattern: get_xml_custom_service_variable_status()
|
||||
*
|
||||
* @param object $customvars XML object with customvar array (from get_xml_custom_service_variable_status)
|
||||
* @return bool True if trackvolume is set to 1 or "true"
|
||||
*/
|
||||
function has_trackvolume_enabled($customvars)
|
||||
{
|
||||
if (!$customvars || !isset($customvars->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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user