Commit 0a4c5fc1 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

Initial commit for persistent model

parent a512bffe
Loading
Loading
Loading
Loading
+1 −0
Changes for fm/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -5,3 +5,4 @@ Three-part spec; read in order.
* [Overview & Architecture](overview-and-architecture.md) — mental model, business view, use cases, scope
* [Requirements & Design](requirements-and-design.md) — requirements, domain model, data model, API design, workflows
* [Implementation & Delivery](implementation-and-delivery.md) — code patterns, security, testing, delivery plan
* [Persistence Model](persistence-model.md) — domain model, fm_db schema, DataBus roles, build status
+301 −0
Changes for fm/persistence-model.md: 301 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Persistence Model
title: "FM Persistence Model"
description: "Federation Manager domain model and storage design: entities, fm_db schema, DataBus roles, and operational guidance."
tags: [fm, federation, persistence]
timestamp: 2026-08-06T00:00:00Z
component: Federation Manager (FM)
version: "2.0"
status: "ETSI SDG OOP Release 2 — living draft, tracked against the FM rewrite"
---

# FM Persistence Model

**Companion documents:** [fm/overview-and-architecture.md](/fm/overview-and-architecture.md), [fm/requirements-and-design.md](/fm/requirements-and-design.md), [fm/implementation-and-delivery.md](/fm/implementation-and-delivery.md)

> **Status: living draft.** This document promotes the data model from
> [requirements-and-design.md](/fm/requirements-and-design.md) §J–§K into a first-class
> persistence model (mirroring [oeg/persistence-model.md](/oeg/persistence-model.md) and
> [srm/persistence-model.md](/srm/persistence-model.md)). It is the authoritative FM schema and
> is expected to change as the FM rewrite lands each table. §"Implementation Status" tracks what
> is actually built versus specified.

## Persistence Scope

This document owns:

- the conceptual model and entity relationships;
- PostgreSQL tables in `fm_db`;
- DataBus (NATS JetStream) stream roles as part of the durable federation-request story;
- what FM deliberately does **not** persist;
- implementation translation notes and current build status.

## I. Domain Model / Conceptual Model

Keep one distinction in mind, the FM analogue of OEG's:

- **SRM owns fulfillment and infrastructure inventory truth.**
- **FM owns the cross-operator relationship and the federation-request audit story around that fulfillment.**

### What `fm_db` is the source of truth for

`fm_db` is authoritative for exactly three things:

1. **Who our federation partners are** — partner profiles, the mTLS trust material (thumbprint for
   inbound, cert/key/CA references for outbound), and connection status.
2. **What each partnership is allowed to do** — federation agreements (permitted API types, zones,
   usage limits, validity) and the partner-app → local `service_specification_id` mappings
   (ADR-0018) that drive SRM dispatch, plus the routing rules that resolve identifiers to partners.
3. **What has crossed the boundary** — the audit record of every inbound and outbound EWBI
   transaction, and the correlation between the external EWBI handle and the internal SRM
   `operation_id`.

The tie-breaker rule (mirrors OEG): **if SRM owns the data, FM holds only a reference (a UUID),
never the data itself.** FM stores `service_specification_id`, zone UUIDs, and `operation_id` as
references; it never stores SRM inventory, placement, or capacity.

### Core entities

**PartnerOP** — an external operator platform FM federates with over the EWBI. Holds EWBI base
URL, inbound `cert_thumbprint`, outbound mTLS material (cert/key/CA path references), supported API
families, SLA limits, and connection status. Exactly one current row per partner in `partner_ops`;
updates mutate in place (no immutable version history in Release 2.0).

**FederationAgreement** — the rules governing what may be shared with one partner: permitted API
types, permitted zones, usage limits, validity window, and — crucially — the `service_spec_mappings`
that resolve a partner-facing `appId` / API family to a local `service_specification_id` (ADR-0018).
It is the resolution authority for SRM dispatch. Stored in `federation_agreements`.

**RoutingRule** — a mapping from an identifier range (`msisdn_prefix`, `ip_cidr`) to a partner OP,
used for outbound federation routing. Stored in `routing_rules`.

**SharedResourceCatalogue** — edge zones/capabilities a partner has published to us or we to a
partner; lets FM validate that a federated deployment targets an authorized zone. Stored in
`shared_resource_catalogues`.

**FederationTransaction** — the durable audit record of one EWBI interaction (inbound or outbound).
Carries the opaque partner-facing `federation_operation_id`, the internal `operation_id` (SRM
correlation), the internal `correlation_id`, the external `federation_correlation_id`, status, and
sanitized request/response summaries. Written `pending` **before** any DataBus publish; the async
retry and SRM-correlation anchor. Stored in `federation_transactions`.

### Relationships

```text
partner_ops            1 ──── * federation_agreements
partner_ops            1 ──── * routing_rules
partner_ops            1 ──── * shared_resource_catalogues
partner_ops            1 ──── * federation_transactions
federation_agreements  1 ──── * federation_transactions
```

### Entity ownership (stores vs. references)

The tie-breaker for "does FM persist this?". FM persists the relationship/agreement/audit story;
anything owned by SRM is held only as a UUID reference.

| # | Entity | Table | FM owns? | Rationale |
|---|---|---|---|---|
| 1 | Partner OP profiles | `partner_ops` | ✅ Yes | Identity + mTLS trust material + connection status for each federated operator. |
| 2 | Federation agreements | `federation_agreements` | ✅ Yes | Permissions + `appId → service_specification_id` mappings (ADR-0018); the self-contained federation contract. |
| 3 | Routing rules | `routing_rules` | ✅ Yes | Resolve MSISDN prefix / IP CIDR → partner for outbound routing. |
| 4 | Shared resource catalogues | `shared_resource_catalogues` | ✅ Yes | Zone/capability availability exchanged with partners; validates federated deployment targets. |
| 5 | Federation transactions | `federation_transactions` | ✅ Yes | Audit + dispute-resolution record; correlates EWBI handle ↔ internal `operation_id`. |
| 6 | Edge cloud zones | *(none)* | ❌ No | SRM authoritative. FM reads via `GET /internal/zones`; stores only zone UUIDs when catalogued. |
| 7 | Service specifications / manifests | *(none)* | ❌ No | SRM owns the catalog. FM stores only the `service_specification_id` reference inside an agreement. |
| 8 | Service instance detail | *(none)* | ❌ No | SRM owns the instance record. FM keeps only `operation_id`/result summary in a transaction. |
| 9 | Capacity / reservations | *(none)* | ❌ No | SRM holds no stored capacity (ADR-0002); FM's `POST /internal/capacity-check` is best-effort, not persisted. |

### What is intentionally not persisted

| Concern | Why not persisted |
|---|---|
| Per-partner rate-limit counters | In-process per-pod counters (REQ-FED-11); no shared store in Release 2.0. |
| Partner / agreement / routing caches | **Not cached at all** — this data must be strongly consistent (suspending a partner takes effect next request), so it is read from PostgreSQL per request (G1). |
| Raw certificate material | `partner_ops` stores path/secret references, not inline cert/key bytes. |
| SRM inventory, placement, capacity, topology | SRM-owned; FM reads live via `/internal/*` or consumes `event.srm.*`. |
| Intermediate operation progress | Phase 1 tracks `pending → in_progress → terminal`; SRM emits no `in_progress` (K.2). |

## J. Data Model and Storage Design

- **PostgreSQL (`fm_db`)** stores all durable FM state.
- **NATS JetStream** carries async federation commands and SRM completion signals.
- **No Redis** — FM keeps no cross-request disposable control data in Release 2.0 (rate limits are
  in-process per pod; partner/agreement/routing data is deliberately uncached, see above).

### J.1 PostgreSQL (`fm_db`)

#### `partner_ops`

| Column | Type | Constraints | Description |
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | Internal partner identifier; used as `app_provider_id` on commands (ADR-0018) |
| `name` | VARCHAR(255) | NOT NULL | Human-readable partner name |
| `mcc_mnc` | VARCHAR(10) | NOT NULL, UNIQUE | MCC+MNC; used as `federation_partner_ref` |
| `base_url` | TEXT | NOT NULL | Partner's EWBI base URL |
| `cert_thumbprint` | VARCHAR(128) | NOT NULL | SHA-256 thumbprint of partner's client cert (inbound mTLS lookup key) |
| `our_client_cert_path` | TEXT | | Ref to our client cert for outbound mTLS to this partner |
| `our_client_key_path` | TEXT | | Ref to our client private key for outbound mTLS |
| `partner_ca_cert_path` | TEXT | | Ref to CA bundle verifying this partner's server cert |
| `supported_apis` | JSONB | NOT NULL, DEFAULT '[]' | API families the partner supports |
| `sla_config` | JSONB | DEFAULT '{}' | SLA parameters (max rps, max concurrent) |
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending' | `pending`, `active`, `suspended`, `decommissioned` |
| `last_heartbeat_at` | TIMESTAMPTZ | | Last successful heartbeat |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |

Indexes: `idx_partner_ops_mcc_mnc`, `idx_partner_ops_status`, `idx_partner_ops_cert_thumbprint`.

#### `federation_agreements`

| Column | Type | Constraints | Description |
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | |
| `partner_op_id` | UUID | NOT NULL, FK → partner_ops(id) | |
| `permitted_api_types` | JSONB | NOT NULL | API types allowed under this agreement |
| `permitted_zone_ids` | JSONB | DEFAULT '[]' | Edge zone UUIDs (`zone.id`, ADR-0017) the partner may access |
| `service_spec_mappings` | JSONB | NOT NULL, DEFAULT '{}' | Partner `appId` / API family → local `service_specification_id` (ADR-0018) |
| `usage_limits` | JSONB | DEFAULT '{}' | Rate/volume limits per API type |
| `valid_from` | TIMESTAMPTZ | NOT NULL | |
| `valid_until` | TIMESTAMPTZ | | NULL = indefinite |
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'draft' | `draft`, `active`, `suspended`, `expired` |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |

Indexes: `idx_federation_agreements_partner`, `idx_federation_agreements_status_validity` on `(status, valid_from, valid_until)`.

#### `routing_rules`

| Column | Type | Constraints | Description |
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | |
| `partner_op_id` | UUID | NOT NULL, FK → partner_ops(id) | Target partner |
| `identifier_type` | VARCHAR(20) | NOT NULL | `msisdn_prefix` or `ip_cidr` |
| `value_range` | VARCHAR(50) | NOT NULL | e.g. `+34`, `203.0.113.0/24` |
| `priority` | INTEGER | NOT NULL, DEFAULT 100 | Lower = higher priority for overlaps |
| `is_active` | BOOLEAN | NOT NULL, DEFAULT true | |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |

Indexes: `idx_routing_rules_type_value` on `(identifier_type, value_range)`, `idx_routing_rules_partner`, `idx_routing_rules_active` WHERE `is_active`. Constraint: `UNIQUE(identifier_type, value_range, priority)`.

#### `shared_resource_catalogues`

| Column | Type | Constraints | Description |
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | |
| `partner_op_id` | UUID | NOT NULL, FK → partner_ops(id) | |
| `direction` | VARCHAR(10) | NOT NULL | `inbound` (partner→us) or `outbound` (us→partner) |
| `resource_type` | VARCHAR(50) | NOT NULL | `edge_zone`, `network_capability`, … |
| `resource_data` | JSONB | NOT NULL | Zone details, capability metadata, availability |
| `synced_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |

Indexes: `idx_shared_catalogue_partner_direction` on `(partner_op_id, direction)`, `idx_shared_catalogue_resource_type`.

#### `federation_transactions`

| Column | Type | Constraints | Description |
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | |
| `partner_op_id` | UUID | NOT NULL, FK → partner_ops(id) | |
| `agreement_id` | UUID | FK → federation_agreements(id) | Governing agreement |
| `direction` | VARCHAR(10) | NOT NULL | `inbound` or `outbound` |
| `federation_operation_id` | UUID | | Opaque handle returned to partner as EWBI `operationId`; minted independently of `operation_id`. NULL for sync queries |
| `operation_id` | UUID | | SRM correlation when an async command was dispatched; NULL for sync queries. Never returned to partners |
| `correlation_id` | UUID | | Internal trace ID; mapped to/from `federation_correlation_id` at the boundary. Never returned to partners |
| `federation_correlation_id` | VARCHAR(255) | NOT NULL | External EWBI correlation ID |
| `api_type` | VARCHAR(100) | NOT NULL | e.g. `edge-cloud-deploy`, `qod-session` |
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending' | lowercase: `pending`, `in_progress`, `completed`, `partially_completed`, `failed`. EWBI JSON exposes uppercase |
| `request_summary` | JSONB | NOT NULL | Sanitized request metadata |
| `response_summary` | JSONB | | Sanitized response metadata |
| `error_detail` | JSONB | | RFC 7807 error if failed |
| `started_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `completed_at` | TIMESTAMPTZ | | |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |

Indexes: `idx_fed_tx_partner`, `UNIQUE (federation_operation_id) WHERE federation_operation_id IS NOT NULL` (poll lookup key for `GET /ewbi/v1/service/operations/{operationId}`), `idx_fed_tx_operation`, `idx_fed_tx_status`, `idx_fed_tx_started`, `idx_fed_tx_fed_correlation`.

Retention: configurable per operator policy; consider partitioning by `started_at` quarter.

### J.2 NATS (DataBus)

FM publishes commands and consumes SRM events; the client-visible source of truth for federation
status remains `federation_transactions` in PostgreSQL.

| Stream | Subjects | Role |
|---|---|---|
| `OOP_TASKS` | `command.srm.service.{deploy,scale,terminate}`, `command.srm.network.capability.{activate,update,deactivate}` | FM publishes; SRM consumes. Every command carries `CommandEnvelopeV1` with `source=federation`, `federation_partner_ref`, `app_provider_id=partner.id` (ADR-0001, ADR-0018) |
| `OOP_EVENTS` | `event.srm.operation.status`, `event.srm.operation.completed`, `event.srm.catalog.updated` | FM consumes via durable `fm-event-worker` (explicit ack, max-deliver 3) |

**Delivery guarantee:** FM writes the `federation_transactions` row (`pending`) **before** publishing;
on successful publish it sets `in_progress` locally. Duplicates are absorbed by SRM's
`service_order.UNIQUE(operation_id)` idempotency anchor.

## O. Implementation Translation

FM follows the same hexagonal layout as OEG/SRM (ADR-0016; single Python/FastAPI service).

```
federation-manager/
├── src/federation_manager/
│   ├── core/                     # config, logging
│   ├── domain/
│   │   ├── models.py             # PartnerOP, Agreement, RoutingRule, Transaction entities + rules
│   │   ├── ports.py              # Protocols for repos, SRM client, DataBus publisher
│   │   ├── errors.py             # federation errors (no HTTP detail)
│   │   ├── topology_hiding.py    # sanitize rules
│   │   └── routing.py            # MSISDN prefix / IP CIDR resolution
│   ├── application/              # inbound / outbound / partner / agreement / catalogue services
│   ├── adapters/
│   │   ├── database/             # SQLAlchemy async: engine, tables, per-entity repos
│   │   ├── databus/              # command.srm.* publisher, event.srm.* consumer
│   │   └── http/                 # SRM internal client, outbound EWBI client
│   ├── api/
│   │   ├── ewbi/v1/              # partner-facing EWBI routes
│   │   ├── internal/             # operator-facing admin routes
│   │   └── platform/health.py
│   ├── middleware/               # mtls_auth, topology_hiding, federation_correlation
│   ├── dependencies.py           # DI wiring (repo/adapter selection)
│   └── main.py                   # composition root + lifespan
└── tests/  unit | integration | e2e
```

- **SQLAlchemy 2.0 async** session for `fm_db`; schema-init-on-startup for Release 2 (Alembic deferred).
- **NATS JetStream** publisher/durable consumer via `nats-py`.
- **Unit of work:** persist `federation_transactions` (`pending`) **before** publish; on publish set
  `in_progress`; finalize from `event.srm.operation.completed`.
- Command/event contract types (`CommandEnvelopeV1`, `Srm*V1`) are vendored per-component and
  validated against [srm/interface-contract.md](/srm/interface-contract.md) (ADR-0027 *proposed*).

## P. Security Design

| Topic | Approach |
|---|---|
| Inbound AuthN | mTLS; Nginx terminates and forwards the client-cert thumbprint; FM matches `partner_ops.cert_thumbprint` |
| Admin AuthN/Z | Keycloak JWT, `operator` role, on `/internal/*` admin routes only |
| Outbound AuthN | our client cert (per-partner refs in `partner_ops`) verifying the partner CA |
| Topology hiding | mandatory `TopologyHider` on every EWBI response (REQ-FED-02); strips internal IPs, cluster/namespace/node names, pod UIDs, trace IDs |
| Correlation boundary | internal `correlation_id` ↔ external `X-Federation-Correlation-ID`; internal IDs never leave |
| Header trust | FM must be reachable only via Nginx, which must strip any client-supplied `X-Client-Certificate-Thumbprint` |

Reference: OPG.02 §2.4 (topology hiding), §4.2.2.3 (E/WBI authentication + agreed-resource exposure); OPG.04 EWBI.

## Implementation Status

Tracks the FM rewrite (branch `feature/r2-fastapi-rewrite`). Update as tables land.

| Table | Schema in code | Repo adapter | Notes |
|---|---|---|---|
| `partner_ops` | Partial (id, mcc_mnc, cert_thumbprint, status) | `PostgresPartnerRepo.find_by_cert_thumbprint` | mTLS material + SLA columns pending |
| `federation_agreements` | Not yet | — | Needed for deploy flow (ADR-0018 mapping) |
| `routing_rules` | Not yet | — | Outbound federation (Phase 2/3) |
| `shared_resource_catalogues` | Not yet | — | Catalogue sync (Phase 2) |
| `federation_transactions` | Not yet | — | Needed before first `command.srm.*` publish |

## Revision History

| Version | Date | Summary |
|---------|------|---------|
| 2.0-draft | 2026-08-06 | Initial FM persistence model, promoted from requirements-and-design §J–§K. |