Add hardware-ingest-json and submodule-integration contracts; expand go-database cursor safety
Synthesized from bible-local reviews across bee, logpile, core, chart, PriceForge: - rules/patterns/hardware-ingest-json/contract.md — Reanimator JSON ingest schema v2.10 - rules/patterns/submodule-integration/contract.md — read-only submodule principle - go-database: add driver-level violation symptoms for cursor safety rule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ See `README.md` for examples, migration snippets, and Docker test commands.
|
||||
|
||||
- Never execute SQL on the same transaction while iterating an open result cursor. Use a two-phase flow: read all rows, close the cursor, then execute writes.
|
||||
- This rule applies to `database/sql`, GORM transactions, and any repository call made while another cursor in the same transaction is still open.
|
||||
- Violation symptoms: `[mysql] invalid connection`, `unexpected EOF`, `driver: bad connection` in Go logs; `Got an error reading communication packets` in MariaDB/MySQL error log. These are driver-level failures, not application errors — the root cause is always a nested SQL call on an open cursor.
|
||||
- Recompute/rebuild/repair flows are the most common violation sites: audit them explicitly.
|
||||
- User-visible records use soft delete or archive flags. Do not hard-delete records with history or foreign-key references.
|
||||
- Archive operations must be reversible from the UI.
|
||||
- Use `gorm:"-"` only for fields that must be ignored entirely. Use `gorm:"-:migration"` for fields populated by queries but excluded from migrations.
|
||||
|
||||
293
rules/patterns/hardware-ingest-json/contract.md
Normal file
293
rules/patterns/hardware-ingest-json/contract.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Contract: Hardware Ingest JSON
|
||||
|
||||
Version: 2.10
|
||||
Source: `bee/bible-local/docs/hardware-ingest-contract.md` (canonical upstream)
|
||||
|
||||
Стандартный JSON-контракт для передачи данных об аппаратном обеспечении серверов в Reanimator.
|
||||
Используется в `bee`, `logpile`, `core` и внешних интеграторах (Redfish-коллекторы, CMDB-экспортёры).
|
||||
|
||||
> Актуальная версия: https://git.mchus.pro/reanimator/core/src/branch/main/bible-local/docs/hardware-ingest-contract.md
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Snapshot** — JSON описывает состояние сервера на момент сбора. Может включать историю изменений статуса.
|
||||
2. **Идемпотентность** — повторная отправка идентичного payload не создаёт дублей (дедупликация по хешу).
|
||||
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
|
||||
4. **Строгая схема** — endpoint использует строгий JSON-декодер; неизвестные поля приводят к `400 Bad Request`.
|
||||
5. **Event-driven** — импорт создаёт события в timeline (LOG_COLLECTED, INSTALLED, REMOVED, FIRMWARE_CHANGED и др.).
|
||||
6. **Без синтеза** — сборщик передаёт только фактически собранные значения. Запрещено придумывать `serial_number`, `component_ref`, `message`, `message_id` или иные идентификаторы, если источник их не предоставил.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
POST /ingest/hardware
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Ответ `202 Accepted` с `job_id`. Результат: `GET /ingest/hardware/jobs/{job_id}`.
|
||||
|
||||
## Структура верхнего уровня
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "redfish://10.10.10.103",
|
||||
"source_type": "api",
|
||||
"protocol": "redfish",
|
||||
"target_host": "10.10.10.103",
|
||||
"collected_at": "2026-02-10T15:30:00Z",
|
||||
"hardware": {
|
||||
"board": { ... },
|
||||
"firmware": [ ... ],
|
||||
"cpus": [ ... ],
|
||||
"memory": [ ... ],
|
||||
"storage": [ ... ],
|
||||
"pcie_devices": [ ... ],
|
||||
"power_supplies": [ ... ],
|
||||
"sensors": { ... },
|
||||
"event_logs": [ ... ],
|
||||
"platform_config": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `collected_at` | string RFC3339 | **да** | Время сбора данных |
|
||||
| `hardware` | object | **да** | Аппаратный снапшот |
|
||||
| `hardware.board.serial_number` | string | **да** | Серийный номер платы/сервера |
|
||||
| `target_host` | string | нет | IP или hostname |
|
||||
| `source_type` | string | нет | `api`, `logfile`, `manual` |
|
||||
| `protocol` | string | нет | `redfish`, `ipmi`, `snmp`, `ssh` |
|
||||
| `filename` | string | нет | Идентификатор источника |
|
||||
|
||||
## Общие поля статуса компонентов
|
||||
|
||||
Применяются ко всем компонентным секциям (`cpus`, `memory`, `storage`, `pcie_devices`, `power_supplies`).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `status` | string | `OK`, `Warning`, `Critical`, `Unknown`, `Empty` |
|
||||
| `status_checked_at` | string RFC3339 | Время последней проверки |
|
||||
| `status_changed_at` | string RFC3339 | Время последнего изменения |
|
||||
| `status_history` | array | История переходов (`status`, `changed_at` обязательны) |
|
||||
| `error_description` | string | Текст ошибки/диагностики |
|
||||
| `manufactured_year_week` | string | Дата производства `YYYY-Www`, например `2024-W07` |
|
||||
|
||||
Правила статуса:
|
||||
- Не включайте записи `status_history` без `changed_at`.
|
||||
- `status_history` сортировать по `changed_at` по возрастанию.
|
||||
- Все даты — RFC3339, рекомендуется UTC (`Z`).
|
||||
|
||||
| Статус | Поведение |
|
||||
|--------|-----------|
|
||||
| `OK` | Нормальная обработка |
|
||||
| `Warning` | Создаётся событие `COMPONENT_WARNING` |
|
||||
| `Critical` | Создаётся событие `COMPONENT_FAILED` + запись в `failure_events` |
|
||||
| `Unknown` | Компонент считается рабочим, создаётся событие `COMPONENT_UNKNOWN` |
|
||||
| `Empty` | Компонент не создаётся/не обновляется |
|
||||
|
||||
## Секции hardware
|
||||
|
||||
### board (обязательная)
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `serial_number` | string | **да** | Серийный номер (ключ идентификации Asset) |
|
||||
| `manufacturer` | string | нет | Производитель |
|
||||
| `product_name` | string | нет | Модель |
|
||||
| `part_number` | string | нет | Партномер |
|
||||
| `uuid` | string | нет | UUID системы |
|
||||
|
||||
Значения `"NULL"` в строковых полях трактуются как отсутствие данных.
|
||||
|
||||
### firmware
|
||||
|
||||
| Поле | Тип | Обязательно |
|
||||
|------|-----|-------------|
|
||||
| `device_name` | string | **да** |
|
||||
| `version` | string | **да** |
|
||||
|
||||
Записи с пустым `device_name` или `version` игнорируются. Изменение версии создаёт событие `FIRMWARE_CHANGED`.
|
||||
|
||||
### cpus
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `socket` | int | **обязательно**; используется для генерации serial |
|
||||
| `model` | string | Модель процессора |
|
||||
| `manufacturer` | string | |
|
||||
| `cores` / `threads` | int | |
|
||||
| `frequency_mhz` / `max_frequency_mhz` | int | |
|
||||
| `temperature_c` | float | Telemetry, °C |
|
||||
| `power_w` | float | Telemetry, Вт |
|
||||
| `throttled` | bool | Thermal/power throttling |
|
||||
| `correctable_error_count` / `uncorrectable_error_count` | int | |
|
||||
| `life_remaining_pct` / `life_used_pct` | float | Health/wear, % |
|
||||
| `serial_number` | string | Если доступен |
|
||||
| `firmware` | string | Версия микрокода (Microcode level — передавать как есть) |
|
||||
| `present` | bool | По умолчанию `true` |
|
||||
|
||||
Генерация serial при отсутствии: `{board_serial}-CPU-{socket}`
|
||||
|
||||
### memory
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `slot` | string | |
|
||||
| `present` | bool | По умолчанию `true` |
|
||||
| `serial_number` | string | Обязателен для создания записи |
|
||||
| `part_number` | string | Используется как модель |
|
||||
| `manufacturer` | string | |
|
||||
| `size_mb` | int | |
|
||||
| `type` | string | `DDR3`, `DDR4`, `DDR5` |
|
||||
| `max_speed_mhz` / `current_speed_mhz` | int | |
|
||||
| `temperature_c` | float | Telemetry |
|
||||
| `correctable_ecc_error_count` / `uncorrectable_ecc_error_count` | int | |
|
||||
| `life_remaining_pct` / `life_used_pct` / `spare_blocks_remaining_pct` | float | |
|
||||
| `performance_degraded` / `data_loss_detected` | bool | |
|
||||
|
||||
Модуль без `serial_number`, с `present=false` или `status=Empty` игнорируется.
|
||||
|
||||
### storage
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `slot` | string | BDF (`0000:18:00.0`) для PCIe-подключённых |
|
||||
| `serial_number` | string | Обязателен для создания записи |
|
||||
| `model` / `manufacturer` | string | |
|
||||
| `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 | Физический размер блока |
|
||||
| `metadata_bytes_per_block` | int64 | Metadata/protection bytes на блок, например `0` или `8` |
|
||||
| `temperature_c` | float | Telemetry |
|
||||
| `power_on_hours` / `power_cycles` / `unsafe_shutdowns` | int64 | |
|
||||
| `media_errors` / `error_log_entries` | int64 | |
|
||||
| `written_bytes` / `read_bytes` | int64 | |
|
||||
| `life_used_pct` / `life_remaining_pct` / `available_spare_pct` | float | |
|
||||
| `reallocated_sectors` / `current_pending_sectors` / `offline_uncorrectable` | int64 | |
|
||||
| `firmware` | string | Изменение создаёт `FIRMWARE_CHANGED` |
|
||||
| `present` | bool | По умолчанию `true` |
|
||||
|
||||
Формат `512+8` не передаётся строкой — только через `logical_block_size_bytes` + `metadata_bytes_per_block`.
|
||||
Диск без `serial_number` игнорируется.
|
||||
|
||||
### pcie_devices
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `slot` | string | Канонический адрес (BDF). `bdf` — deprecated alias, нормализуется при ingest |
|
||||
| `vendor_id` / `device_id` | int | PCI ID (decimal) |
|
||||
| `numa_node` | int | NUMA/CPU affinity |
|
||||
| `device_class` | string | `MassStorageController`, `StorageController`, `NetworkController`, `EthernetController`, `FibreChannelController`, `VideoController`, `ProcessingAccelerator`, `DisplayController` (список открытый) |
|
||||
| `manufacturer` / `model` / `serial_number` / `firmware` | string | |
|
||||
| `link_width` / `max_link_width` | int | |
|
||||
| `link_speed` / `max_link_speed` | string | `Gen3`, `Gen4`, `Gen5` |
|
||||
| `mac_addresses` | string[] | MAC-адреса портов |
|
||||
| `temperature_c` / `power_w` | float | Device-level telemetry |
|
||||
| `life_remaining_pct` / `life_used_pct` | float | |
|
||||
| `ecc_corrected_total` / `ecc_uncorrected_total` | int64 | |
|
||||
| `hw_slowdown` | bool | |
|
||||
| `battery_charge_pct` / `battery_health_pct` / `battery_temperature_c` / `battery_voltage_v` | float | |
|
||||
| `battery_replace_required` | bool | |
|
||||
| `sfp_temperature_c` / `sfp_tx_power_dbm` / `sfp_rx_power_dbm` / `sfp_voltage_v` / `sfp_bias_ma` | float | Optical telemetry |
|
||||
| `present` | bool | По умолчанию `true` |
|
||||
|
||||
Генерация serial при отсутствии или `"N/A"`: `{board_serial}-PCIE-{slot}`
|
||||
|
||||
### power_supplies
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `slot` | string | |
|
||||
| `present` | bool | По умолчанию `true` |
|
||||
| `serial_number` | string | Обязателен для создания записи |
|
||||
| `part_number` / `model` / `vendor` | string | |
|
||||
| `wattage_w` | int | |
|
||||
| `firmware` | string | |
|
||||
| `input_type` | string | Например `ACWideRange` |
|
||||
| `input_voltage` / `input_power_w` / `output_power_w` / `temperature_c` | float | Telemetry |
|
||||
| `life_remaining_pct` / `life_used_pct` | float | |
|
||||
|
||||
PSU без `serial_number` игнорируется.
|
||||
|
||||
### sensors (опционально)
|
||||
|
||||
Данные хранятся как last-known-value на уровне Asset. Идентификатор: `(sensor_type, name)`.
|
||||
Поле `location` передавать не нужно — игнорируется. Сенсоры без `name` игнорируются.
|
||||
|
||||
```json
|
||||
"sensors": {
|
||||
"fans": [{ "name": "FAN1", "rpm": 4200, "status": "OK" }],
|
||||
"power": [{ "name": "12V Rail", "voltage_v": 12.06, "status": "OK" }],
|
||||
"temperatures": [{ "name": "CPU0 Temp", "celsius": 46.0, "threshold_warning_celsius": 80.0, "threshold_critical_celsius": 95.0, "status": "OK" }],
|
||||
"other": [{ "name": "System Humidity", "value": 38.5, "unit": "%" }]
|
||||
}
|
||||
```
|
||||
|
||||
### event_logs (опционально)
|
||||
|
||||
Нормализованные операционные логи. Не попадают в history timeline. Дедуплицируются по `(asset, source, fingerprint)`.
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| `source` | string | **да** | `host`, `bmc`, `redfish` |
|
||||
| `message` | string | **да** | Нормализованный текст события |
|
||||
| `event_time` | string RFC3339 | нет | |
|
||||
| `severity` | string | нет | `OK`, `Info`, `Warning`, `Critical`, `Unknown` |
|
||||
| `message_id` | string | нет | Код события источника |
|
||||
| `component_ref` | string | нет | Ссылка на компонент/слот |
|
||||
| `fingerprint` | string | нет | Внешний dedup-key; если нет — система вычисляет свой |
|
||||
| `is_active` | bool | нет | Событие всё ещё активно |
|
||||
| `raw_payload` | object | нет | Сырой vendor-specific payload |
|
||||
|
||||
Запрещено синтезировать `message`, `message_id`, `component_ref`, serial/device identifiers.
|
||||
|
||||
### platform_config (опционально)
|
||||
|
||||
Произвольный объект с настройками платформы (BIOS/Redfish/IPMI) как есть из источника.
|
||||
При каждом импорте хранится latest-snapshot per machine.
|
||||
|
||||
## Обработка отсутствующих serial_number
|
||||
|
||||
Интегратор не подставляет вымышленные значения, хеши или placeholder-идентификаторы.
|
||||
Разрешены только server-side fallback-правила:
|
||||
|
||||
| Тип | Поведение |
|
||||
|-----|-----------|
|
||||
| CPU | Генерируется: `{board_serial}-CPU-{socket}` |
|
||||
| PCIe | Генерируется: `{board_serial}-PCIE-{slot}` |
|
||||
| Memory | Компонент игнорируется |
|
||||
| Storage | Компонент игнорируется |
|
||||
| PSU | Компонент игнорируется |
|
||||
|
||||
Если `serial_number` не уникален внутри payload для того же `model`: первое вхождение — оригинальный serial, дубли получают `NO_SN-XXXXXXXX`.
|
||||
|
||||
## Минимальный валидный пример
|
||||
|
||||
```json
|
||||
{
|
||||
"collected_at": "2026-02-10T15:30:00Z",
|
||||
"target_host": "192.168.1.100",
|
||||
"hardware": {
|
||||
"board": {
|
||||
"serial_number": "SRV-001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
| Версия | Дата | Изменения |
|
||||
|--------|------|-----------|
|
||||
| 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` |
|
||||
| 2.8 | 2026-03-15 | Поле `location` удалено из всех `sensors.*` |
|
||||
| 2.7 | 2026-03-15 | Явно запрещён синтез данных в `event_logs` |
|
||||
| 2.6 | 2026-03-15 | Добавлена секция `event_logs` |
|
||||
| 2.5 | 2026-03-15 | Добавлено `manufactured_year_week` для всех компонентов |
|
||||
| 2.4 | 2026-03-15 | Component telemetry: health/life поля для всех секций |
|
||||
| 2.0 | 2026-02-01 | `status_history`, `status_changed_at`; async job response |
|
||||
| 1.0 | 2026-01-01 | Начальная версия |
|
||||
41
rules/patterns/submodule-integration/contract.md
Normal file
41
rules/patterns/submodule-integration/contract.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Contract: Git Submodule Integration
|
||||
|
||||
Version: 1.0
|
||||
|
||||
Правила для проектов, использующих git submodules (shared libraries, viewers, bible, tooling).
|
||||
|
||||
Применяется в: `bee` (internal/chart/, bible/), `chart`, `logpile/internal/chart/`, `PriceForge`.
|
||||
|
||||
## Основное правило
|
||||
|
||||
**Embedded submodules — read-only с точки зрения host-проекта.**
|
||||
|
||||
## Запрещено
|
||||
|
||||
- Реализовывать project-specific поведение путём редактирования кода submodule.
|
||||
- Вносить в submodule изменения, специфичные для одного host-проекта.
|
||||
- Держать в submodule локальные неотправленные коммиты как часть feature host-проекта.
|
||||
|
||||
## Разрешено
|
||||
|
||||
- Обновлять указатель submodule на upstream-коммит после merge там.
|
||||
- Если нужна новая возможность в submodule — предложить и влить её в upstream как generic-изменение, затем подтянуть через обновление указателя.
|
||||
|
||||
## Когда нужны новые данные
|
||||
|
||||
Если host-проект нуждается в новых данных, которые должен отображать submodule-viewer:
|
||||
|
||||
1. Производить, нормализовывать и сериализовывать новые данные в самом host-проекте.
|
||||
2. Обновить JSON-контракт (например, `bible-local/docs/hardware-ingest-contract.md`), чтобы viewer мог читать их из стандартного snapshot.
|
||||
3. Предложить поддержку нового поля в upstream viewer как generic-изменение.
|
||||
|
||||
## Почему
|
||||
|
||||
Конкретный провал: попытка добавить telemetry storage в `bee` через редактирование `internal/chart/` создала coupling shared viewer с одним host-проектом и риск скрытых регрессий в других проектах, использующих тот же `chart`.
|
||||
|
||||
## Документирование интеграции
|
||||
|
||||
В `bible-local/` host-проекта должен быть явный контракт:
|
||||
- Какие данные ожидает submodule на входе.
|
||||
- Как host-проект их производит (какой модуль/файл).
|
||||
- Текущий upstream commit/tag submodule.
|
||||
Reference in New Issue
Block a user