# 07 — Exporters & Reanimator Integration

## Export endpoints summary

| Endpoint | Format | Filename pattern |
|----------|--------|-----------------|
| `GET /api/export/csv` | CSV — serial numbers | `YYYY-MM-DD (MODEL) - SN.csv` |
| `GET /api/export/json` | Full `AnalysisResult` JSON (incl. `raw_payloads`) | `YYYY-MM-DD (MODEL) - SN.json` |
| `GET /api/export/reanimator` | Reanimator hardware JSON | `YYYY-MM-DD (MODEL) - SN.json` |

---

## Reanimator Export

### Purpose

Exports hardware inventory data in the format expected by the Reanimator asset tracking
system. Enables one-click push from LOGPile to an external asset management platform.

### Implementation files

| File | Role |
|------|------|
| `internal/exporter/reanimator_models.go` | Go structs for Reanimator JSON |
| `internal/exporter/reanimator_converter.go` | `ConvertToReanimator()` and helpers |
| `internal/server/handlers.go` | `handleExportReanimator()` HTTP handler |

### Conversion rules

- Source: canonical `hardware.devices` repository (see [`04-data-models.md`](04-data-models.md))
- CPU manufacturer inferred from model string (Intel / AMD / ARM / Ampere)
- PCIe serial number generated when absent: `{board_serial}-PCIE-{slot}`
- Status values normalized to: `OK`, `Warning`, `Critical`, `Unknown` (`Empty` only for memory slots)
- Timestamps in RFC3339 format
- `target_host` derived from `filename` field (`redfish://…`, `ipmi://…`) if not in source; omitted if undeterminable
- `board.manufacturer` and `board.product_name` values of `"NULL"` treated as absent

### LOGPile → Reanimator field mapping

| LOGPile type | Reanimator section | Notes |
|---|---|---|
| `BoardInfo` | `board` | Direct mapping |
| `CPU` | `cpus` | + manufacturer (inferred) |
| `MemoryDIMM` | `memory` | Direct; empty slots included (`present=false`) |
| `Storage` | `storage` | Excluded if no `serial_number` |
| `PCIeDevice` | `pcie_devices` | Serial generated if missing |
| `GPU` | `pcie_devices` | `device_class=DisplayController` |
| `NetworkAdapter` | `pcie_devices` | `device_class=NetworkController` |
| `PSU` | `power_supplies` | Excluded if no serial or `present=false` |
| `FirmwareInfo` | `firmware` | Direct mapping |

### Inclusion / exclusion rules

**Included:**
- Memory slots with `present=false` (as Empty slots)
- PCIe devices without serial number (serial is generated)

**Excluded:**
- Storage without `serial_number`
- PSU without `serial_number` or with `present=false`
- NetworkAdapters with `present=false`

---

## Reanimator Integration Guide

This section documents the Reanimator receiver-side JSON format (what the Reanimator
system expects when it ingests a LOGPile export).

> **Important:** The Reanimator endpoint uses a strict JSON decoder (`DisallowUnknownFields`).
> Any unknown field — including nested ones — causes `400 Bad Request`.
> Use only `snake_case` keys listed here.

### Top-level structure

```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": [...]
  }
}
```

**Required:** `collected_at`, `hardware.board.serial_number`
**Optional:** `target_host`, `source_type`, `protocol`, `filename`

`source_type` values: `api`, `logfile`, `manual`
`protocol` values: `redfish`, `ipmi`, `snmp`, `ssh`

### Component status fields (all component sections)

Each component may carry:

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | `OK`, `Warning`, `Critical`, `Unknown`, `Empty` |
| `status_checked_at` | RFC3339 | When status was last verified |
| `status_changed_at` | RFC3339 | When status last changed |
| `status_at_collection` | object | `{ "status": "...", "at": "..." }` — snapshot-time status |
| `status_history` | array | `[{ "status": "...", "changed_at": "...", "details": "..." }]` |
| `error_description` | string | Human-readable error for Warning/Critical |

### Board

```json
{
  "board": {
    "manufacturer": "Supermicro",
    "product_name": "X12DPG-QT6",
    "serial_number": "21D634101",
    "part_number": "X12DPG-QT6-REV1.01",
    "uuid": "d7ef2fe5-2fd0-11f0-910a-346f11040868"
  }
}
```

`serial_number` required. `manufacturer` / `product_name` of `"NULL"` treated as absent.

### CPUs

```json
{
  "socket": 0,
  "model": "INTEL(R) XEON(R) GOLD 6530",
  "cores": 32,
  "threads": 64,
  "frequency_mhz": 2100,
  "max_frequency_mhz": 4000,
  "manufacturer": "Intel",
  "status": "OK"
}
```

`socket` (int) and `model` required. Serial generated: `{board_serial}-CPU-{socket}`.

LOT format: `CPU_{VENDOR}_{MODEL_NORMALIZED}` → e.g. `CPU_INTEL_XEON_GOLD_6530`

### Memory

```json
{
  "slot": "CPU0_C0D0",
  "location": "CPU0_C0D0",
  "present": true,
  "size_mb": 32768,
  "type": "DDR5",
  "max_speed_mhz": 4800,
  "current_speed_mhz": 4800,
  "manufacturer": "Hynix",
  "serial_number": "80AD032419E17CEEC1",
  "part_number": "HMCG88AGBRA191N",
  "status": "OK"
}
```

`slot` and `present` required. `serial_number` required when `present=true`.
Empty slots (`present=false`, `status="Empty"`) are included but no component created.

LOT format: `DIMM_{TYPE}_{SIZE_GB}GB` → e.g. `DIMM_DDR5_32GB`

### Storage

```json
{
  "slot": "OB01",
  "type": "NVMe",
  "model": "INTEL SSDPF2KX076T1",
  "size_gb": 7680,
  "serial_number": "BTAX41900GF87P6DGN",
  "manufacturer": "Intel",
  "firmware": "9CV10510",
  "interface": "NVMe",
  "present": true,
  "status": "OK"
}
```

`slot`, `model`, `serial_number`, `present` required.

LOT format: `{TYPE}_{INTERFACE}_{SIZE_TB}TB` → e.g. `SSD_NVME_07.68TB`

### Power Supplies

```json
{
  "slot": "0",
  "present": true,
  "model": "GW-CRPS3000LW",
  "vendor": "Great Wall",
  "wattage_w": 3000,
  "serial_number": "2P06C102610",
  "part_number": "V0310C9000000000",
  "firmware": "00.03.05",
  "status": "OK",
  "input_power_w": 137,
  "output_power_w": 104,
  "input_voltage": 215.25
}
```

`slot`, `present` required. `serial_number` required when `present=true`.
Telemetry fields (`input_power_w`, `output_power_w`, `input_voltage`) stored in observation only.

LOT format: `PSU_{WATTAGE}W_{VENDOR_NORMALIZED}` → e.g. `PSU_3000W_GREAT_WALL`

### PCIe Devices

```json
{
  "slot": "PCIeCard1",
  "vendor_id": 32902,
  "device_id": 2912,
  "bdf": "0000:18:00.0",
  "device_class": "MassStorageController",
  "manufacturer": "Intel",
  "model": "RAID Controller RSP3DD080F",
  "link_width": 8,
  "link_speed": "Gen3",
  "max_link_width": 8,
  "max_link_speed": "Gen3",
  "serial_number": "RAID-001-12345",
  "firmware": "50.9.1-4296",
  "status": "OK"
}
```

`slot` required. Serial generated if absent: `{board_serial}-PCIE-{slot}`.

`device_class` values: `NetworkController`, `MassStorageController`, `DisplayController`, etc.

LOT format: `PCIE_{DEVICE_CLASS}_{MODEL_NORMALIZED}` → e.g. `PCIE_NETWORK_CONNECTX5`

### Firmware

```json
[
  { "device_name": "BIOS", "version": "06.08.05" },
  { "device_name": "BMC",  "version": "5.17.00" }
]
```

Both fields required. Changes trigger `FIRMWARE_CHANGED` timeline events.

---

### Import process (Reanimator side)

1. Validate `collected_at` (RFC3339) and `hardware.board.serial_number`.
2. Find or create Asset by `board.serial_number` → `vendor_serial`.
3. For each component: filter `present=false`, auto-determine LOT, find or create Component,
   create Observation, update Installations.
4. Detect removed components (present in previous snapshot, absent in current) → close Installation.
5. Generate timeline events: `LOG_COLLECTED`, `INSTALLED`, `REMOVED`, `FIRMWARE_CHANGED`.

**Idempotency:** Repeated import of the same snapshot (same content hash) returns `200 OK`
with `"duplicate": true` and does not create duplicate records.

### Reanimator API endpoint

```http
POST /ingest/hardware
Content-Type: application/json
```

**Success (201):**
```json
{
  "status": "success",
  "bundle_id": "lb_01J...",
  "asset_id": "mach_01J...",
  "collected_at": "2026-02-10T15:30:00Z",
  "duplicate": false,
  "summary": {
    "parts_observed": 15,
    "parts_created": 2,
    "installations_created": 2,
    "timeline_events_created": 9
  }
}
```

**Duplicate (200):**
```json
{ "status": "success", "duplicate": true, "message": "LogBundle with this content hash already exists" }
```

**Error (400):**
```json
{ "status": "error", "error": "validation_failed", "details": { "field": "...", "message": "..." } }
```

Common `400` causes:
- Unknown JSON field (strict decoder)
- Wrong key name (e.g. `targetHost` instead of `target_host`)
- Invalid `collected_at` format (must be RFC3339)
- Empty `hardware.board.serial_number`

### LOT normalization rules

1. Remove special chars `( ) - ® ™`; replace spaces with `_`
2. Uppercase all
3. Collapse multiple underscores to one
4. Strip common prefixes like `MODEL:`, `PN:`

### Status values

| Value | Meaning | Action |
|-------|---------|--------|
| `OK` | Normal | — |
| `Warning` | Degraded | Create `COMPONENT_WARNING` event (optional) |
| `Critical` | Failed | Auto-create `failure_event`, create `COMPONENT_FAILED` event |
| `Unknown` | Not determinable | Treat as working |
| `Empty` | Slot unpopulated | No component created (memory/PCIe only) |

### Missing field handling

| Field | Fallback |
|-------|---------|
| CPU serial | Generated: `{board_serial}-CPU-{socket}` |
| PCIe serial | Generated: `{board_serial}-PCIE-{slot}` |
| Other serial | Component skipped if absent |
| manufacturer (PCIe) | Looked up from `vendor_id` (8086→Intel, 10de→NVIDIA, 15b3→Mellanox…) |
| status | Treated as `Unknown` |
| firmware | No `FIRMWARE_CHANGED` event |
