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:
2026-06-12 10:17:50 +03:00
parent 0005f3e41a
commit 1977730d93
3 changed files with 336 additions and 0 deletions

View File

@@ -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.

View 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 | Начальная версия |

View 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.