Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ce0251ce4 | ||
|
|
994d46f3b3 | ||
|
|
ee3e8a6e33 | ||
|
|
e2c81758b5 | ||
|
|
6b52a1876f | ||
|
|
3e3c48bc08 |
2
bible
2
bible
Submodule bible updated: d2600f1279...1977730d93
21
bible-local/BACKLOG.md
Normal file
21
bible-local/BACKLOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Backlog
|
||||
|
||||
## [sfp_modules] Поддержка per-port SFP/QSFP модулей в экспорте Reanimator
|
||||
|
||||
**Приоритет:** низкий (до выхода Reanimator v3.0, пока deprecated sfp_* скаляры ещё принимаются)
|
||||
|
||||
**Контекст:**
|
||||
Reanimator Hardware Ingest Contract v2.11 вводит массив `pcie_devices[].sfp_modules[]` для передачи данных SFP/QSFP-модулей по портам. Старые скалярные поля (`sfp_temperature_c`, `sfp_tx_power_dbm`, `sfp_rx_power_dbm`, `sfp_voltage_v`, `sfp_bias_ma`) помечены deprecated и будут удалены в v3.0. Для многопортовых NIC (ConnectX-6 Dx, Intel X710 и подобных) текущая реализация теряет данные — коллектор берёт первое найденное значение и не знает о портах.
|
||||
|
||||
**Текущее состояние:**
|
||||
- Коллектор (`internal/collector/redfish.go`, `redfishPCIeDetailsWithSupplementalDocs`) собирает SFP как 5 скалярных `float64` на устройство через `redfishFirstNumericAcrossDocs`
|
||||
- Внутренняя модель (`internal/models/models.go`, struct `PCIeDevice`) не имеет SFP-полей — всё хранится в `Details map[string]any`
|
||||
- Конвертер (`internal/exporter/reanimator_converter.go`, строки 864–868) читает скаляры из `Details` и кладёт в deprecated поля `ReanimatorPCIe`
|
||||
|
||||
**Что нужно сделать:**
|
||||
1. **Исследование** — проверить, отдают ли реальные Redfish-источники SFP-данные per-port и в каком виде (прежде чем менять модель)
|
||||
2. **Коллектор** (`redfish.go`) — если Redfish отдаёт per-port данные, собирать их в массив с индексом порта
|
||||
3. **Внутренняя модель** (`models.go`) — добавить `SFPModules []SFPModule` в `PCIeDevice`
|
||||
4. **Экспорт** (`reanimator_models.go`, `reanimator_converter.go`) — добавить `ReanimatorSFPModule`, смапить `SFPModules` в `sfp_modules[]`; убрать deprecated скаляры
|
||||
|
||||
**Триггер для реализации:** анонс Reanimator v3.0 с удалением deprecated sfp_* полей.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Hardware Ingest JSON Contract
|
||||
version: "2.7"
|
||||
updated: "2026-03-15"
|
||||
version: "2.11"
|
||||
updated: "2026-06-19"
|
||||
maintainer: Reanimator Core
|
||||
audience: external-integrators, ai-agents
|
||||
language: ru
|
||||
@@ -9,7 +9,7 @@ language: ru
|
||||
|
||||
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
|
||||
|
||||
Версия: **2.7** · Дата: **2026-03-15**
|
||||
Версия: **2.11** · Дата: **2026-06-19**
|
||||
|
||||
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
|
||||
Предназначен для разработчиков смежных систем (Redfish-коллекторов, агентов мониторинга, CMDB-экспортёров) и может быть включён в документацию интегрируемых проектов.
|
||||
@@ -22,6 +22,10 @@ language: ru
|
||||
|
||||
| Версия | Дата | Изменения |
|
||||
|--------|------|-----------|
|
||||
| 2.11 | 2026-06-19 | В `pcie_devices[]` добавлен необязательный массив `sfp_modules[]` с идентификацией и DOM telemetry SFP/QSFP-модулей. Скалярные поля `sfp_temperature_c` / `sfp_tx_power_dbm` / `sfp_rx_power_dbm` / `sfp_voltage_v` / `sfp_bias_ma` помечены как deprecated (принимаются, но `sfp_modules[]` имеет приоритет) |
|
||||
| 2.10 | 2026-04-29 | Для `hardware.storage[]` добавлены необязательные числовые поля `logical_block_size_bytes`, `physical_block_size_bytes`, `metadata_bytes_per_block` для нормализованного описания формата блока накопителя |
|
||||
| 2.9 | 2026-03-19 | Добавлена необязательная секция `hardware.platform_config` — произвольный объект с настройками платформы (BIOS/Redfish); хранится как latest-snapshot per machine |
|
||||
| 2.8 | 2026-03-15 | Поле `location` удалено из всех `sensors.*`; сенсоры передаются только по `name` и измеренным значениям |
|
||||
| 2.7 | 2026-03-15 | Явно запрещён синтез данных в `event_logs`; интеграторы не должны придумывать серийные номера компонентов, если источник их не отдал |
|
||||
| 2.6 | 2026-03-15 | Добавлена необязательная секция `event_logs` для dedup/upsert логов `host` / `bmc` / `redfish` вне history timeline |
|
||||
| 2.5 | 2026-03-15 | Добавлено общее необязательное поле `manufactured_year_week` для компонентных секций (`YYYY-Www`) |
|
||||
@@ -131,8 +135,9 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
"storage": [ ... ],
|
||||
"pcie_devices": [ ... ],
|
||||
"power_supplies": [ ... ],
|
||||
"sensors": { ... },
|
||||
"event_logs": [ ... ]
|
||||
"sensors": { ... },
|
||||
"event_logs": [ ... ],
|
||||
"platform_config": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -343,6 +348,9 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
| `type` | string | нет | Тип: `NVMe`, `SSD`, `HDD` |
|
||||
| `interface` | string | нет | Интерфейс: `NVMe`, `SATA`, `SAS` |
|
||||
| `size_gb` | int | нет | Размер в ГБ |
|
||||
| `logical_block_size_bytes` | int64 | нет | Логический размер пользовательского блока данных, например `512` или `4096` |
|
||||
| `physical_block_size_bytes` | int64 | нет | Физический размер блока, если известен, например `4096` |
|
||||
| `metadata_bytes_per_block` | int64 | нет | Metadata / protection bytes на логический блок, например `0` или `8` |
|
||||
| `temperature_c` | float | нет | Температура накопителя, °C (telemetry) |
|
||||
| `power_on_hours` | int64 | нет | Время работы, часы |
|
||||
| `power_cycles` | int64 | нет | Количество циклов питания |
|
||||
@@ -363,6 +371,11 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
|
||||
Диск без `serial_number` игнорируется. Изменение `firmware` создаёт событие `FIRMWARE_CHANGED`.
|
||||
|
||||
Формат вида `512+8` в контракт не добавляется отдельным строковым полем. Если источник знает такую форму, он должен передавать её как:
|
||||
- `logical_block_size_bytes = 512`
|
||||
- `metadata_bytes_per_block = 8`
|
||||
- `physical_block_size_bytes = 512` или `4096`, если известен физический размер блока
|
||||
|
||||
```json
|
||||
"storage": [
|
||||
{
|
||||
@@ -370,6 +383,9 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
"type": "NVMe",
|
||||
"model": "INTEL SSDPF2KX076T1",
|
||||
"size_gb": 7680,
|
||||
"logical_block_size_bytes": 512,
|
||||
"physical_block_size_bytes": 4096,
|
||||
"metadata_bytes_per_block": 8,
|
||||
"temperature_c": 38.5,
|
||||
"power_on_hours": 12450,
|
||||
"unsafe_shutdowns": 3,
|
||||
@@ -407,11 +423,12 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
| `battery_temperature_c` | float | нет | Температура батареи / supercap, °C |
|
||||
| `battery_voltage_v` | float | нет | Напряжение батареи / supercap, В |
|
||||
| `battery_replace_required` | bool | нет | Требуется замена батареи / supercap |
|
||||
| `sfp_temperature_c` | float | нет | Температура SFP/optic, °C |
|
||||
| `sfp_tx_power_dbm` | float | нет | TX optical power, dBm |
|
||||
| `sfp_rx_power_dbm` | float | нет | RX optical power, dBm |
|
||||
| `sfp_voltage_v` | float | нет | Напряжение SFP, В |
|
||||
| `sfp_bias_ma` | float | нет | Bias current SFP, мА |
|
||||
| `sfp_temperature_c` | float | нет | Температура SFP/optic, °C *(deprecated since 2.11)* |
|
||||
| `sfp_tx_power_dbm` | float | нет | TX optical power, dBm *(deprecated since 2.11)* |
|
||||
| `sfp_rx_power_dbm` | float | нет | RX optical power, dBm *(deprecated since 2.11)* |
|
||||
| `sfp_voltage_v` | float | нет | Напряжение SFP, В *(deprecated since 2.11)* |
|
||||
| `sfp_bias_ma` | float | нет | Bias current SFP, мА *(deprecated since 2.11)* |
|
||||
| `sfp_modules` | array | нет | Установленные SFP/QSFP-модули по портам (см. sfp_modules[]) |
|
||||
| `bdf` | string | нет | Deprecated alias для `slot`; при наличии ingest нормализует его в `slot` |
|
||||
| `device_class` | string | нет | Класс устройства (см. список ниже) |
|
||||
| `manufacturer` | string | нет | Производитель |
|
||||
@@ -429,10 +446,43 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
`numa_node` передавайте для NIC / InfiniBand / RAID / GPU, когда источник знает CPU/NUMA affinity. Поле сохраняется в snapshot-атрибутах PCIe-компонента и дублируется в telemetry для topology use cases.
|
||||
Поля `temperature_c` и `power_w` используйте для device-level telemetry GPU / accelerator / smart PCIe devices. Они не влияют на идентификацию компонента.
|
||||
|
||||
**Deprecated поля sfp_\*:** Скалярные поля `sfp_temperature_c`, `sfp_tx_power_dbm`, `sfp_rx_power_dbm`, `sfp_voltage_v`, `sfp_bias_ma` продолжают приниматься, но помечены как deprecated since 2.11. Если в payload одновременно присутствуют `sfp_modules[]` и deprecated sfp_-скаляры — приоритет у `sfp_modules[]`, скаляры игнорируются. Deprecated поля будут удалены в версии 3.0.
|
||||
|
||||
**Генерация serial_number при отсутствии или `"N/A"`:** `{board_serial}-PCIE-{slot}`, где `slot` для PCIe равен BDF.
|
||||
|
||||
`slot` — единственный канонический адрес компонента. Для PCIe в `slot` передавайте BDF. Поле `bdf` сохраняется только как переходный alias на входе и не должно использоваться как отдельная координата рядом со `slot`.
|
||||
|
||||
#### pcie_devices[].sfp_modules[]
|
||||
|
||||
Необязательный массив установленных SFP/QSFP-модулей для данного PCIe-устройства. Один элемент — один порт. Используйте для многопортовых NIC (ConnectX-6 Dx, Intel X710, Mellanox HDR и др.).
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `port` | int | **да** | Номер порта на NIC (0-based). Ключ дедупликации внутри устройства |
|
||||
| `identifier` | string | нет | Тип модуля: `SFP`, `SFP+`, `SFP28`, `QSFP+`, `QSFP28`, `QSFP-DD`, `DAC` |
|
||||
| `connector` | string | нет | Тип разъёма: `LC`, `MPO`, `RJ45`, `DAC`, `AOC`, `No separable connector` |
|
||||
| `vendor` | string | нет | Производитель модуля из EEPROM |
|
||||
| `part_number` | string | нет | Партномер из EEPROM |
|
||||
| `serial_number` | string | нет | Серийный номер из EEPROM |
|
||||
| `revision` | string | нет | Ревизия из EEPROM |
|
||||
| `wavelength_nm` | int | нет | Длина волны, нм (0 для DAC/медных кабелей) |
|
||||
| `transceiver_type` | string | нет | `10GBase-SR`, `10GBase-LR`, `25GBase-SR`, `100GBase-SR4`, `DAC`, … |
|
||||
| `temperature_c` | float | нет | Температура модуля, °C (DOM telemetry) |
|
||||
| `voltage_v` | float | нет | Напряжение питания, В (DOM telemetry) |
|
||||
| `tx_power_dbm` | float | нет | TX оптическая мощность, dBm (DOM telemetry) |
|
||||
| `rx_power_dbm` | float | нет | RX оптическая мощность, dBm (DOM telemetry) |
|
||||
| `bias_ma` | float | нет | Bias current, мА (DOM telemetry) |
|
||||
|
||||
**Ключ дедупликации:** `(pcie_devices[].slot, sfp_modules[].port)`.
|
||||
|
||||
**Правила ingest:**
|
||||
- При каждом импорте — полная замена `sfp_modules[]` для данного `pcie_devices[].slot` (upsert всего массива целиком).
|
||||
- Если `sfp_modules` отсутствует или `null` — существующие данные SFP не трогать.
|
||||
- Если `sfp_modules: []` (пустой массив) — трактовать как «модули не обнаружены», очистить сохранённые данные.
|
||||
- Дубли по `port` внутри одного `pcie_devices[]` — невалидны, endpoint возвращает `400` с описанием поля.
|
||||
- Модули без `serial_number` допустимы (многие DAC-кабели не имеют SN); сохраняются по ключу `(slot, port)`.
|
||||
- Изменение `serial_number` или `part_number` модуля на порту создаёт событие `COMPONENT_CHANGED` для PCIe-устройства с описанием «SFP module replaced on port N».
|
||||
|
||||
**Значения `device_class`:**
|
||||
|
||||
| Значение | Назначение |
|
||||
@@ -457,16 +507,47 @@ GET /ingest/hardware/jobs/{job_id}
|
||||
"numa_node": 0,
|
||||
"temperature_c": 48.5,
|
||||
"power_w": 18.2,
|
||||
"sfp_temperature_c": 36.2,
|
||||
"sfp_tx_power_dbm": -1.8,
|
||||
"sfp_rx_power_dbm": -2.1,
|
||||
"device_class": "EthernetController",
|
||||
"manufacturer": "Intel",
|
||||
"model": "X710 10GbE",
|
||||
"serial_number": "K65472-003",
|
||||
"firmware": "9.20 0x8000d4ae",
|
||||
"manufacturer": "Mellanox",
|
||||
"model": "ConnectX-6 Dx",
|
||||
"serial_number": "MT2012X12345",
|
||||
"firmware": "22.35.2010",
|
||||
"mac_addresses": ["3c:fd:fe:aa:bb:cc", "3c:fd:fe:aa:bb:cd"],
|
||||
"status": "OK"
|
||||
"status": "OK",
|
||||
"sfp_modules": [
|
||||
{
|
||||
"port": 0,
|
||||
"identifier": "QSFP28",
|
||||
"connector": "LC",
|
||||
"vendor": "Mellanox",
|
||||
"part_number": "MFA1A00-C003",
|
||||
"serial_number": "MT2124VS09999",
|
||||
"revision": "A",
|
||||
"wavelength_nm": 850,
|
||||
"transceiver_type": "100GBase-SR4",
|
||||
"temperature_c": 36.4,
|
||||
"voltage_v": 3.29,
|
||||
"tx_power_dbm": -1.8,
|
||||
"rx_power_dbm": -2.1,
|
||||
"bias_ma": 7.2
|
||||
},
|
||||
{
|
||||
"port": 1,
|
||||
"identifier": "QSFP28",
|
||||
"connector": "LC",
|
||||
"vendor": "Mellanox",
|
||||
"part_number": "MFA1A00-C003",
|
||||
"serial_number": "MT2124VS09998",
|
||||
"revision": "A",
|
||||
"wavelength_nm": 850,
|
||||
"transceiver_type": "100GBase-SR4",
|
||||
"temperature_c": 35.9,
|
||||
"voltage_v": 3.28,
|
||||
"tx_power_dbm": -1.9,
|
||||
"rx_power_dbm": -2.3,
|
||||
"bias_ma": 7.1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -592,7 +673,6 @@ PSU без `serial_number` игнорируется.
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `name` | string | **да** | Уникальное имя сенсора в рамках секции |
|
||||
| `location` | string | нет | Физическое расположение |
|
||||
| `rpm` | int | нет | Обороты, RPM |
|
||||
| `status` | string | нет | Статус: `OK`, `Warning`, `Critical`, `Unknown` |
|
||||
|
||||
@@ -601,7 +681,6 @@ PSU без `serial_number` игнорируется.
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `name` | string | **да** | Уникальное имя сенсора |
|
||||
| `location` | string | нет | Физическое расположение |
|
||||
| `voltage_v` | float | нет | Напряжение, В |
|
||||
| `current_a` | float | нет | Ток, А |
|
||||
| `power_w` | float | нет | Мощность, Вт |
|
||||
@@ -612,7 +691,6 @@ PSU без `serial_number` игнорируется.
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `name` | string | **да** | Уникальное имя сенсора |
|
||||
| `location` | string | нет | Физическое расположение |
|
||||
| `celsius` | float | нет | Температура, °C |
|
||||
| `threshold_warning_celsius` | float | нет | Порог Warning, °C |
|
||||
| `threshold_critical_celsius` | float | нет | Порог Critical, °C |
|
||||
@@ -623,38 +701,63 @@ PSU без `serial_number` игнорируется.
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `name` | string | **да** | Уникальное имя сенсора |
|
||||
| `location` | string | нет | Физическое расположение |
|
||||
| `value` | float | нет | Значение |
|
||||
| `unit` | string | нет | Единица измерения |
|
||||
| `status` | string | нет | Статус |
|
||||
|
||||
**Правила sensors:**
|
||||
- Идентификатор сенсора: пара `(sensor_type, name)`. Дубли в одном payload — берётся первое вхождение.
|
||||
- `location` для сенсоров передавать не нужно и не следует: в Reanimator location/slot используется только для проверки перемещения и установки компонентов, а не для last-known-value sensor ingest.
|
||||
- Сенсоры без `name` игнорируются.
|
||||
- При каждом импорте значения перезаписываются (upsert по ключу).
|
||||
|
||||
```json
|
||||
"sensors": {
|
||||
"fans": [
|
||||
{ "name": "FAN1", "location": "Front", "rpm": 4200, "status": "OK" },
|
||||
{ "name": "FAN_CPU0", "location": "CPU0", "rpm": 5600, "status": "OK" }
|
||||
{ "name": "FAN1", "rpm": 4200, "status": "OK" },
|
||||
{ "name": "FAN_CPU0", "rpm": 5600, "status": "OK" }
|
||||
],
|
||||
"power": [
|
||||
{ "name": "12V Rail", "location": "Mainboard", "voltage_v": 12.06, "status": "OK" },
|
||||
{ "name": "PSU0 Input", "location": "PSU0", "voltage_v": 215.25, "current_a": 0.64, "power_w": 137.0, "status": "OK" }
|
||||
{ "name": "12V Rail", "voltage_v": 12.06, "status": "OK" },
|
||||
{ "name": "PSU0 Input", "voltage_v": 215.25, "current_a": 0.64, "power_w": 137.0, "status": "OK" }
|
||||
],
|
||||
"temperatures": [
|
||||
{ "name": "CPU0 Temp", "location": "CPU0", "celsius": 46.0, "threshold_warning_celsius": 80.0, "threshold_critical_celsius": 95.0, "status": "OK" },
|
||||
{ "name": "Inlet Temp", "location": "Front", "celsius": 22.0, "threshold_warning_celsius": 40.0, "threshold_critical_celsius": 50.0, "status": "OK" }
|
||||
{ "name": "CPU0 Temp", "celsius": 46.0, "threshold_warning_celsius": 80.0, "threshold_critical_celsius": 95.0, "status": "OK" },
|
||||
{ "name": "Inlet Temp", "celsius": 22.0, "threshold_warning_celsius": 40.0, "threshold_critical_celsius": 50.0, "status": "OK" }
|
||||
],
|
||||
"other": [
|
||||
{ "name": "System Humidity", "value": 38.5, "unit": "%" , "status": "OK" }
|
||||
{ "name": "System Humidity", "value": 38.5, "unit": "%", "status": "OK" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Секция platform_config
|
||||
|
||||
Необязательный объект с произвольными ключами — настройки платформы как есть из источника (BIOS, Redfish, IPMI).
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `platform_config` | object | нет | Произвольный объект: ключи — строки, значения — строки, числа, булевы |
|
||||
|
||||
**Правила platform_config:**
|
||||
- Содержимое объекта не валидируется: передавайте параметры как есть.
|
||||
- При каждом импорте хранится latest-snapshot per machine; история изменений по каждому ключу накапливается отдельно.
|
||||
- Если секция отсутствует или равна `null` — данные платформы не обновляются.
|
||||
|
||||
```json
|
||||
"platform_config": {
|
||||
"SecureBoot": "Enabled",
|
||||
"BiosVersion": "06.08.05",
|
||||
"TpmEnabled": true,
|
||||
"NumaEnabled": false,
|
||||
"HyperThreading": "Enabled"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обработка статусов компонентов
|
||||
|
||||
| Статус | Поведение |
|
||||
@@ -756,7 +859,24 @@ PSU без `serial_number` игнорируется.
|
||||
"model": "X710 10GbE",
|
||||
"serial_number": "K65472-003",
|
||||
"mac_addresses": ["3c:fd:fe:aa:bb:cc", "3c:fd:fe:aa:bb:cd"],
|
||||
"status": "OK"
|
||||
"status": "OK",
|
||||
"sfp_modules": [
|
||||
{
|
||||
"port": 0,
|
||||
"identifier": "SFP+",
|
||||
"connector": "LC",
|
||||
"vendor": "Intel",
|
||||
"part_number": "FTLX8574D3BCV-IT",
|
||||
"serial_number": "FNS123456789",
|
||||
"wavelength_nm": 850,
|
||||
"transceiver_type": "10GBase-SR",
|
||||
"temperature_c": 34.1,
|
||||
"voltage_v": 3.30,
|
||||
"tx_power_dbm": -2.5,
|
||||
"rx_power_dbm": -3.0,
|
||||
"bias_ma": 6.8
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"power_supplies": [
|
||||
@@ -787,6 +907,12 @@ PSU без `serial_number` игнорируется.
|
||||
"other": [
|
||||
{ "name": "System Humidity", "value": 38.5, "unit": "%" }
|
||||
]
|
||||
},
|
||||
"platform_config": {
|
||||
"SecureBoot": "Enabled",
|
||||
"BiosVersion": "06.08.05",
|
||||
"TpmEnabled": true,
|
||||
"HyperThreading": "Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule internal/chart updated: 8105c7ec08...8c80591531
@@ -244,7 +244,7 @@ func isReplayStorageServiceEndpoint(doc map[string]interface{}, dev models.PCIeD
|
||||
if strings.Contains(name, "pcie switch management endpoint") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(name, "volume management device nvme raid controller") {
|
||||
if strings.Contains(name, "volume management device") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -174,3 +174,42 @@ func firstNonEmptyString(values ...string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExportLogsCSV writes all recognized events as a semicolon-delimited UTF-8 CSV readable in Excel.
|
||||
func ExportLogsCSV(w io.Writer, result *models.AnalysisResult) error {
|
||||
if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
||||
return err
|
||||
}
|
||||
writer := csv.NewWriter(w)
|
||||
writer.Comma = ';'
|
||||
defer writer.Flush()
|
||||
|
||||
if err := writer.Write([]string{"timestamp", "source", "severity", "sensor_type", "sensor_name", "event_type", "id", "description", "raw_data"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, e := range result.Events {
|
||||
ts := ""
|
||||
if !e.Timestamp.IsZero() {
|
||||
ts = e.Timestamp.UTC().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
ts,
|
||||
e.Source,
|
||||
string(e.Severity),
|
||||
e.SensorType,
|
||||
e.SensorName,
|
||||
e.EventType,
|
||||
e.ID,
|
||||
e.Description,
|
||||
e.RawData,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1235,7 +1235,7 @@ func normalizeEventLogSource(source string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(source)) {
|
||||
case "redfish":
|
||||
return "redfish"
|
||||
case "sel", "bmc", "ipmi", "idrac", "lifecycle controller", "lifecyclecontroller":
|
||||
case "sel", "bmc", "ipmi", "idrac", "lifecycle controller", "lifecyclecontroller", "hpe ilo", "ilo":
|
||||
return "bmc"
|
||||
case "system", "syslog", "smart", "zfs", "file", "gpu", "dmi", "nvidia driver", "gpu field diagnostics", "fan", "memory", "host":
|
||||
return "host"
|
||||
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
)
|
||||
|
||||
const maxSingleFileSize = 10 * 1024 * 1024
|
||||
const maxSingleFileSizeLarge = 1024 * 1024 * 1024
|
||||
const maxZipArchiveSize = 50 * 1024 * 1024
|
||||
const maxGzipDecompressedSize = 50 * 1024 * 1024
|
||||
|
||||
|
||||
var supportedArchiveExt = map[string]struct{}{
|
||||
".ahs": {},
|
||||
".gz": {},
|
||||
@@ -47,7 +49,7 @@ func ExtractArchive(archivePath string) ([]ExtractedFile, error) {
|
||||
|
||||
switch ext {
|
||||
case ".ahs":
|
||||
return extractSingleFile(archivePath)
|
||||
return extractSingleFileWithLimit(archivePath, maxSingleFileSizeLarge)
|
||||
case ".gz", ".tgz":
|
||||
return extractTarGz(archivePath)
|
||||
case ".tar", ".sds":
|
||||
@@ -55,7 +57,7 @@ func ExtractArchive(archivePath string) ([]ExtractedFile, error) {
|
||||
case ".zip":
|
||||
return extractZip(archivePath)
|
||||
case ".txt", ".log":
|
||||
return extractSingleFile(archivePath)
|
||||
return extractSingleFileWithLimit(archivePath, maxSingleFileSize)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported archive format: %s", ext)
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func ExtractArchiveFromReader(r io.Reader, filename string) ([]ExtractedFile, er
|
||||
|
||||
switch ext {
|
||||
case ".ahs":
|
||||
return extractSingleFileFromReader(r, filename)
|
||||
return extractSingleFileFromReaderWithLimit(r, filename, maxSingleFileSizeLarge)
|
||||
case ".gz", ".tgz":
|
||||
return extractTarGzFromReader(r, filename)
|
||||
case ".tar", ".sds":
|
||||
@@ -78,7 +80,7 @@ func ExtractArchiveFromReader(r io.Reader, filename string) ([]ExtractedFile, er
|
||||
case ".zip":
|
||||
return extractZipFromReader(r)
|
||||
case ".txt", ".log":
|
||||
return extractSingleFileFromReader(r, filename)
|
||||
return extractSingleFileFromReaderWithLimit(r, filename, maxSingleFileSize)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported archive format: %s", ext)
|
||||
}
|
||||
@@ -337,7 +339,7 @@ func extractZipFromReader(r io.Reader) ([]ExtractedFile, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func extractSingleFile(path string) ([]ExtractedFile, error) {
|
||||
func extractSingleFileWithLimit(path string, limit int64) ([]ExtractedFile, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat file: %w", err)
|
||||
@@ -348,7 +350,7 @@ func extractSingleFile(path string) ([]ExtractedFile, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
files, err := extractSingleFileFromReader(f, filepath.Base(path))
|
||||
files, err := extractSingleFileFromReaderWithLimit(f, filepath.Base(path), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -358,14 +360,14 @@ func extractSingleFile(path string) ([]ExtractedFile, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func extractSingleFileFromReader(r io.Reader, filename string) ([]ExtractedFile, error) {
|
||||
content, err := io.ReadAll(io.LimitReader(r, maxSingleFileSize+1))
|
||||
func extractSingleFileFromReaderWithLimit(r io.Reader, filename string, limit int64) ([]ExtractedFile, error) {
|
||||
content, err := io.ReadAll(io.LimitReader(r, limit+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file content: %w", err)
|
||||
}
|
||||
truncated := len(content) > maxSingleFileSize
|
||||
truncated := int64(len(content)) > limit
|
||||
if truncated {
|
||||
content = content[:maxSingleFileSize]
|
||||
content = content[:limit]
|
||||
}
|
||||
|
||||
file := ExtractedFile{
|
||||
@@ -376,7 +378,7 @@ func extractSingleFileFromReader(r io.Reader, filename string) ([]ExtractedFile,
|
||||
file.Truncated = true
|
||||
file.TruncatedMessage = fmt.Sprintf(
|
||||
"file exceeded %d bytes and was truncated",
|
||||
maxSingleFileSize,
|
||||
limit,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
81
internal/parser/vendors/hpe_ilo_ahs/parser.go
vendored
81
internal/parser/vendors/hpe_ilo_ahs/parser.go
vendored
@@ -214,8 +214,10 @@ func parseAHSContainer(data []byte) ([]ahsEntry, error) {
|
||||
name := strings.TrimRight(string(data[offset+20:offset+52]), "\x00")
|
||||
start := offset + ahsHeaderSize
|
||||
end := start + size
|
||||
truncated := false
|
||||
if size < 0 || end > len(data) {
|
||||
return nil, fmt.Errorf("invalid payload size for %q", name)
|
||||
end = len(data)
|
||||
truncated = true
|
||||
}
|
||||
|
||||
payload := append([]byte(nil), data[start:end]...)
|
||||
@@ -235,6 +237,9 @@ func parseAHSContainer(data []byte) ([]ahsEntry, error) {
|
||||
Content: content,
|
||||
Compressed: compressed,
|
||||
})
|
||||
if truncated {
|
||||
break
|
||||
}
|
||||
offset = end
|
||||
}
|
||||
|
||||
@@ -992,7 +997,7 @@ func parseEvents(tokens []string) []models.Event {
|
||||
break
|
||||
}
|
||||
if looksLikeEventMessage(tokens[j]) {
|
||||
message = tokens[j]
|
||||
message = trimEventJunk(tokens[j])
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1173,7 +1178,7 @@ func looksLikeServerModel(v string) bool {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(v)
|
||||
return strings.Contains(lower, "proliant") || strings.Contains(lower, "apollo") || strings.Contains(lower, "synergy") || strings.Contains(lower, "edgeline")
|
||||
return strings.Contains(lower, "proliant") || strings.Contains(lower, "apollo") || strings.Contains(lower, "synergy") || strings.Contains(lower, "edgeline") || strings.Contains(lower, "alletra")
|
||||
}
|
||||
|
||||
func looksLikeCPUVendor(v string) bool {
|
||||
@@ -1464,7 +1469,19 @@ func fabricIDFromPath(path string) string {
|
||||
func inferSeverity(message string) models.Severity {
|
||||
lower := strings.ToLower(message)
|
||||
switch {
|
||||
case strings.Contains(lower, " down"), strings.Contains(lower, "warning"), strings.Contains(lower, "fail"), strings.Contains(lower, "error"):
|
||||
case strings.Contains(lower, "critical"):
|
||||
return models.SeverityCritical
|
||||
case strings.Contains(lower, " down"),
|
||||
strings.Contains(lower, "warning"),
|
||||
strings.Contains(lower, "fail"),
|
||||
strings.Contains(lower, "error"),
|
||||
strings.Contains(lower, "server reset"),
|
||||
strings.Contains(lower, "server power"),
|
||||
strings.Contains(lower, "power restored"),
|
||||
strings.Contains(lower, "ilo reset"),
|
||||
strings.Contains(lower, "ilo restarted"),
|
||||
strings.Contains(lower, "pcr measurements"),
|
||||
strings.Contains(lower, "hardware data received from uefi"):
|
||||
return models.SeverityWarning
|
||||
default:
|
||||
return models.SeverityInfo
|
||||
@@ -1478,21 +1495,73 @@ func inferEventType(message string) string {
|
||||
return "Login"
|
||||
case strings.Contains(lower, "logout"):
|
||||
return "Logout"
|
||||
case strings.Contains(lower, "network"):
|
||||
case strings.Contains(lower, "network"), strings.Contains(lower, "link"):
|
||||
return "Network"
|
||||
case strings.Contains(lower, "license"):
|
||||
return "License"
|
||||
case strings.Contains(lower, "backup operation"), strings.Contains(lower, "remote console"):
|
||||
return "Management"
|
||||
case strings.Contains(lower, "server power"), strings.Contains(lower, "power restored"), strings.Contains(lower, "power off"), strings.Contains(lower, "server reset"), strings.Contains(lower, "ilo reset"), strings.Contains(lower, "ilo restarted"):
|
||||
return "Power"
|
||||
case strings.Contains(lower, "storage"), strings.Contains(lower, "volume"), strings.Contains(lower, "drive"), strings.Contains(lower, "firmware"):
|
||||
return "Hardware"
|
||||
case strings.Contains(lower, "certificate"), strings.Contains(lower, "pcr measurements"), strings.Contains(lower, "hardware data"), strings.Contains(lower, "security"):
|
||||
return "Security"
|
||||
default:
|
||||
return "Event"
|
||||
}
|
||||
}
|
||||
|
||||
// trimEventJunk strips trailing single-byte frame markers written by iLO into
|
||||
// binary .zbb log records. These markers are printable ASCII (letters, *, +, ')
|
||||
// that appear immediately after the sentence-ending punctuation or a digit.
|
||||
func trimEventJunk(s string) string {
|
||||
if len(s) < 3 {
|
||||
return s
|
||||
}
|
||||
last := s[len(s)-1]
|
||||
prev := s[len(s)-2]
|
||||
isJunk := (last >= 'A' && last <= 'Z') || (last >= 'a' && last <= 'z') ||
|
||||
last == '*' || last == '+' || last == '\''
|
||||
prevIsBoundary := prev == '.' || prev == '!' || prev == '"' || prev == ')' ||
|
||||
(prev >= '0' && prev <= '9')
|
||||
if isJunk && prevIsBoundary {
|
||||
return s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func looksLikeEventMessage(v string) bool {
|
||||
if len(v) < 8 || strings.HasPrefix(v, "src/") || strings.HasPrefix(v, "PciRoot(") {
|
||||
return false
|
||||
}
|
||||
// JSON document accidentally extracted — skip
|
||||
if strings.HasPrefix(v, "{") || strings.HasPrefix(v, "[") {
|
||||
return false
|
||||
}
|
||||
// Numbered list items (e.g. "2.Perform the iLO reset.") are instructions, not events
|
||||
if len(v) > 2 && v[0] >= '1' && v[0] <= '9' && v[1] == '.' {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(v)
|
||||
return strings.Contains(lower, "login") || strings.Contains(lower, "logout") || strings.Contains(lower, "link") || strings.Contains(lower, "license") || strings.Contains(lower, "security state")
|
||||
return strings.Contains(lower, "login") ||
|
||||
strings.Contains(lower, "logout") ||
|
||||
strings.Contains(lower, "link") ||
|
||||
strings.Contains(lower, "license") ||
|
||||
strings.Contains(lower, "security state") ||
|
||||
strings.Contains(lower, "server power") ||
|
||||
strings.Contains(lower, "server reset") ||
|
||||
strings.Contains(lower, "power restored") ||
|
||||
strings.Contains(lower, "power off") ||
|
||||
strings.Contains(lower, "storage") ||
|
||||
strings.Contains(lower, "firmware") ||
|
||||
strings.Contains(lower, "certificate") ||
|
||||
strings.Contains(lower, "backup operation") ||
|
||||
strings.Contains(lower, "pcr measurements") ||
|
||||
strings.Contains(lower, "hardware data") ||
|
||||
strings.Contains(lower, "ilo reset") ||
|
||||
strings.Contains(lower, "ilo restarted") ||
|
||||
strings.Contains(lower, "remote console")
|
||||
}
|
||||
|
||||
func sanitizeModel(v string) string {
|
||||
|
||||
@@ -153,6 +153,29 @@ func TestParseAHSInventory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAHSTruncatedEntry(t *testing.T) {
|
||||
p := &Parser{}
|
||||
// Build archive where the last entry's declared size exceeds available data.
|
||||
archive := makeAHSArchive(t, []ahsTestEntry{
|
||||
{Name: "CUST_INFO.DAT", Payload: []byte("HPE\x00ProLiant DL380 Gen11\x00CZ2D1X0GS3\x00P52560-421")},
|
||||
{Name: "0000150-2025-11-27.zbb", Payload: []byte("some content")},
|
||||
})
|
||||
// Corrupt the size field of the second entry to exceed len(archive).
|
||||
secondHeaderOffset := ahsHeaderSize + len([]byte("HPE\x00ProLiant DL380 Gen11\x00CZ2D1X0GS3\x00P52560-421"))
|
||||
binary.LittleEndian.PutUint32(archive[secondHeaderOffset+8:secondHeaderOffset+12], 0xFFFFFFFF)
|
||||
|
||||
result, err := p.Parse([]parser.ExtractedFile{{
|
||||
Path: "HPE_CZ2D1X0GS3_20251127.ahs",
|
||||
Content: archive,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("expected graceful handling of truncated entry, got error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExampleAHS(t *testing.T) {
|
||||
path := filepath.Join("..", "..", "..", "..", "example", "HPE_CZ2D1X0GS3_20260330.ahs")
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
2787
internal/parser/vendors/pciids/pci.ids
vendored
2787
internal/parser/vendors/pciids/pci.ids
vendored
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,10 @@ func TestHandleChartCurrent_RendersCurrentReanimatorSnapshot(t *testing.T) {
|
||||
t.Fatalf("expected chart title in body, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, `/chart/static/view.css`) {
|
||||
t.Fatalf("expected rewritten chart static path, got %q", body)
|
||||
t.Fatalf("expected rewritten chart css path, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, `/chart/static/view.js`) {
|
||||
t.Fatalf("expected rewritten chart js path, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "Snapshot Metadata") {
|
||||
t.Fatalf("expected rendered chart output, got %q", body)
|
||||
|
||||
@@ -137,7 +137,9 @@ func chartTitle(result *models.AnalysisResult) string {
|
||||
}
|
||||
|
||||
func rewriteChartStaticPaths(html []byte) []byte {
|
||||
return bytes.ReplaceAll(html, []byte(`href="/static/view.css"`), []byte(`href="/chart/static/view.css"`))
|
||||
html = bytes.ReplaceAll(html, []byte(`href="/static/view.css"`), []byte(`href="/chart/static/view.css"`))
|
||||
html = bytes.ReplaceAll(html, []byte(`src="/static/view.js"`), []byte(`src="/chart/static/view.js"`))
|
||||
return html
|
||||
}
|
||||
|
||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1232,6 +1234,13 @@ func (s *Server) handleExportCSV(w http.ResponseWriter, r *http.Request) {
|
||||
exp.ExportCSV(w)
|
||||
}
|
||||
|
||||
func (s *Server) handleExportLogsCSV(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "logs.csv")))
|
||||
exporter.ExportLogsCSV(w, result)
|
||||
}
|
||||
|
||||
func (s *Server) handleExportJSON(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ func (s *Server) setupRoutes() {
|
||||
s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV)
|
||||
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
||||
s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator)
|
||||
s.mux.HandleFunc("GET /api/export/logs-csv", s.handleExportLogsCSV)
|
||||
s.mux.HandleFunc("POST /api/convert", s.handleConvertReanimatorBatch)
|
||||
s.mux.HandleFunc("GET /api/convert/{id}", s.handleConvertStatus)
|
||||
s.mux.HandleFunc("GET /api/convert/{id}/download", s.handleConvertDownload)
|
||||
|
||||
60
releases/v1.22/RELEASE_NOTES.md
Normal file
60
releases/v1.22/RELEASE_NOTES.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# logpile v1.22
|
||||
|
||||
Дата релиза: 2026-06-19
|
||||
Тег: `v1.22`
|
||||
|
||||
## Что нового
|
||||
|
||||
### HPE iLO AHS — новый парсер
|
||||
|
||||
Добавлена поддержка файлов `*.ahs` (Active Health System), экспортируемых
|
||||
из веб-интерфейса iLO. Парсер извлекает:
|
||||
|
||||
- **Инвентарь оборудования**: плата, процессоры, память, диски, сетевые
|
||||
адаптеры, блоки питания, backplane, RAID-контроллеры.
|
||||
- **Прошивки**: iLO, System ROM, SPS, TPM, SPLD, контроллеры, NIC, backplane —
|
||||
из основного бинарного контейнера и XML-сертификата `bcert.pkg`.
|
||||
- **События**: разбор `.zbb`-файлов с журналом iLO; определение типа и
|
||||
серьёзности по тексту сообщения; очистка однобайтовых frame-сепараторов
|
||||
из концов строк.
|
||||
- **Устойчивость к битым файлам**: если последняя запись в AHS-контейнере
|
||||
обрезана (объявленный размер выходит за границу файла), парсер обрабатывает
|
||||
данные частично вместо возврата ошибки.
|
||||
- Добавлено распознавание модельного ряда **Alletra Storage Server** (ранее
|
||||
`ProductName` оставался пустым).
|
||||
|
||||
### Экспорт логов в CSV («Logs Export»)
|
||||
|
||||
Новая кнопка «**Logs Export**» в шапке интерфейса выгружает все
|
||||
распознанные события (без какой-либо фильтрации) в CSV-файл:
|
||||
|
||||
- Разделитель — точка с запятой (`;`), кодировка — UTF-8 с BOM.
|
||||
- Файл открывается в Excel без дополнительных настроек импорта.
|
||||
- Колонки: `timestamp`, `source`, `severity`, `sensor_type`, `sensor_name`,
|
||||
`event_type`, `id`, `description`, `raw_data`.
|
||||
|
||||
Кнопка «PDF» удалена.
|
||||
|
||||
### Исправления в Reanimator-экспорте
|
||||
|
||||
- `event_logs` в JSON-экспорте Reanimator больше не оказывается пустым для
|
||||
HPE iLO AHS: источник `"HPE iLO"` теперь корректно нормализуется в `"bmc"`.
|
||||
|
||||
### Исправления chart viewer
|
||||
|
||||
- JavaScript `view.js` не загружался в LOGPile из-за отсутствия перезаписи
|
||||
пути `/static/view.js` → `/chart/static/view.js`. Исправлено; фильтры
|
||||
по колонкам в таблицах теперь работают.
|
||||
- Субмодуль chart обновлён до **v2.7**: фильтры вынесены в отдельную строку
|
||||
под заголовком, исправлена минимальная ширина колонок.
|
||||
|
||||
### Обновления зависимостей
|
||||
|
||||
- **pci.ids** (база PCI-устройств) обновлена. Коллектор скорректирован под
|
||||
переименование `0x8086:0x28c0`: `"Volume Management Device NVMe RAID
|
||||
Controller"` → `"Volume Management Device (VMD)"`.
|
||||
|
||||
## Запуск на macOS
|
||||
|
||||
Снимите карантинный атрибут через терминал: `xattr -d com.apple.quarantine /path/to/logpile-darwin-arm64`
|
||||
После этого бинарник запустится без предупреждения Gatekeeper.
|
||||
23
releases/v1.23/RELEASE_NOTES.md
Normal file
23
releases/v1.23/RELEASE_NOTES.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# logpile v1.23
|
||||
|
||||
Дата релиза: 2026-06-19
|
||||
Тег: `v1.23`
|
||||
|
||||
## Что нового
|
||||
|
||||
### Исправление: HPE iLO AHS файлы больше 10 МБ не обрезаются
|
||||
|
||||
AHS-файлы могут весить сотни мегабайт (типичный пример — 104 МБ). Универсальный
|
||||
лимит в 10 МБ молча обрезал их, из-за чего парсер видел лишь начало файла и
|
||||
извлекал неполный список событий.
|
||||
|
||||
Теперь лимит зависит от расширения: `.ahs` — до **1 ГБ**, прочие
|
||||
одиночные файлы (`.txt`, `.log`) — прежние 10 МБ.
|
||||
|
||||
Для AHS-файла размером 104 МБ количество распознанных событий увеличивается
|
||||
с ~529 до ~12 600.
|
||||
|
||||
## Запуск на macOS
|
||||
|
||||
Снимите карантинный атрибут через терминал: `xattr -d com.apple.quarantine /path/to/logpile-darwin-arm64`
|
||||
После этого бинарник запустится без предупреждения Gatekeeper.
|
||||
2
third_party/pciids
vendored
2
third_party/pciids
vendored
Submodule third_party/pciids updated: 9186887530...a18f209e39
@@ -1410,7 +1410,7 @@ async function loadData(vendor, filename) {
|
||||
document.getElementById('clear-btn').classList.remove('hidden');
|
||||
document.getElementById('header-raw-btn').classList.remove('hidden');
|
||||
document.getElementById('header-reanimator-btn').classList.remove('hidden');
|
||||
document.getElementById('header-pdf-btn').classList.remove('hidden');
|
||||
document.getElementById('header-logs-csv-btn').classList.remove('hidden');
|
||||
document.getElementById('header-log-meta').classList.remove('hidden');
|
||||
|
||||
loadAuditViewer();
|
||||
@@ -1510,10 +1510,6 @@ function exportData(format) {
|
||||
window.location.href = `/api/export/${format}`;
|
||||
}
|
||||
|
||||
function printReport() {
|
||||
window.open('/chart/current?print=true', '_blank');
|
||||
}
|
||||
|
||||
// Clear data
|
||||
async function clearData() {
|
||||
try {
|
||||
@@ -1523,7 +1519,7 @@ async function clearData() {
|
||||
document.getElementById('clear-btn').classList.add('hidden');
|
||||
document.getElementById('header-raw-btn').classList.add('hidden');
|
||||
document.getElementById('header-reanimator-btn').classList.add('hidden');
|
||||
document.getElementById('header-pdf-btn').classList.add('hidden');
|
||||
document.getElementById('header-logs-csv-btn').classList.add('hidden');
|
||||
document.getElementById('header-log-meta').classList.add('hidden');
|
||||
document.getElementById('upload-status').textContent = '';
|
||||
const frame = document.getElementById('audit-viewer-frame');
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<button id="clear-btn" class="header-action hidden" onclick="clearData()">Clear Data</button>
|
||||
<button id="header-raw-btn" class="header-action hidden" onclick="exportData('json')">Raw Data</button>
|
||||
<button id="header-reanimator-btn" class="header-action hidden" onclick="exportData('reanimator')">Reanimator</button>
|
||||
<button id="header-pdf-btn" class="header-action hidden" onclick="printReport()">PDF</button>
|
||||
<button id="header-logs-csv-btn" class="header-action hidden" onclick="exportData('logs-csv')">Logs Export</button>
|
||||
<button id="restart-btn" class="header-action" onclick="restartApp()">Restart</button>
|
||||
<button id="exit-btn" class="header-action" onclick="exitApp()">Exit</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user