Architecture¶
The Organization Manager sits between the writers of allocation data (the Django admin portal, customer integrations posting to the REST API) and the IP Mapper that the rest of Platform Filter reads from at query time. PostgreSQL is the source of truth shared between the admin portal and orgmanager; NATS is the transport that carries session updates downstream to the IP Mapper.
Role in Platform Filter¶
flowchart LR
A[Admin portal / API clients] -- REST --> O[orgmanager]
O <-- SQL --> P[(PostgreSQL)]
O -- NATS --> M[ipmapper]
M -- query path --> R[Platform Filter]
- Upstream writers. The Django admin portal in
admin-portalwrites the same PostgreSQL tables directly. Customer integrations drive the REST API. Both paths bumporg_versionon every change. - PostgreSQL. Holds organisations, prefixes, and overrides plus
their
org_versionrow used by the version scanner. SQLite is available in dev and test builds but is not shipped in the production RPM (see Storage backends). - NATS to IP Mapper. Orgmanager publishes session updates and
cleanup requests on subjects under the configured itsy
prefix, targeting theipmapperservice.
Core concepts¶
These terms appear throughout the configuration, REST API, and operations pages. This is the single source of truth for them.
Organisation¶
A tenant. Identified by an opaque org_id string in the URL path
and on the wire. CIDRs are isolated per organisation — two
organisations may have overlapping or identical CIDRs in their own
namespaces without conflict, but two organisations may not own
the same prefix in the global allocation table.
Prefix¶
An IP CIDR block that an organisation has been allocated. Prefixes
are added and removed via PATCH /org/{org}/prefixes. Cross-org
overlap is refused.
Override¶
A specific CIDR rule with attributes — username, location,
deviceid, optional opaque meta. Overrides live within (or on top
of) the organisation's prefixes. An override whose CIDR is not yet
covered by any prefix is stored but marked outside-prefixes and
held in reserve. When a covering prefix is added later, the override
becomes active.
Effective range¶
The post-collapse, non-overlapping output the IP Mapper sees. Overrides are sorted most-specific-first and laid down on top of the prefix tree. Where an override is split by a more specific one, the original CIDR's attributes still apply to the unsplit pieces. There is no attribute inheritance — each range carries the attributes of exactly one source override.
Worked example. Three nested overrides:
10.0.0.0/8 → username: "org-default"
10.1.0.0/16 → username: "department-x", location: "west"
10.1.2.0/24 → username: "team-y"
Collapsed effective ranges:
| Range | username | location |
|---|---|---|
| 10.0.0.0 – 10.0.255.255 | org-default | (none) |
| 10.1.0.0 – 10.1.1.255 | department-x | west |
| 10.1.2.0 – 10.1.2.255 | team-y | (none) |
| 10.1.3.0 – 10.1.255.255 | department-x | west |
| 10.2.0.0 – 10.255.255.255 | org-default | (none) |
team-y does not inherit location: west.
Session¶
One unique combination of (org, CIDR, username, location, deviceid).
Sessions are what orgmanager publishes to the IP Mapper. The session
ID is deterministic in those five inputs — a change to any attribute
yields a new session, which the IP Mapper sees as a stop of the
old plus a start of the new.
Revision¶
A monotonic per-organisation counter attached to session start / update actions. The IP Mapper uses it to clean up sessions that predate a full redeploy or a prefix removal — see Cleanup contract with the IP Mapper.
active vs outside-prefixes¶
Computed at runtime by checking whether an override's CIDR is fully
contained within the union of the organisation's current
prefixes. The status is not stored. The API recomputes it on every
read, so adding or removing a prefix is reflected in _status on
the next read of any covered override; the IP Mapper-side
application of that flip happens on the next deploy.
PostgreSQL concurrency model¶
Orgmanager protects writes at three layers.
Per-organisation in-process mutex¶
Every write path takes a per-org_id mutex inside the orgmanager
process. Concurrent writes to the same organisation serialise; writes
to different organisations run in parallel.
Single-organisation transaction with org_version bump¶
Each write runs inside one PostgreSQL transaction that ends with an
increment of the organisation's org_version. The version scanner
relies on this — a bumped org_version is the signal that a
redeploy is needed, whether the change came from this orgmanager,
another orgmanager, or the Django admin portal.
PostgreSQL advisory lock for prefix overlap¶
Prefix mutations additionally take a transaction-scoped PostgreSQL advisory lock before checking for overlap with other organisations. Without the lock, two prefix-add transactions running under READ COMMITTED could each see no overlap and both commit, leaving two organisations claiming the same address space.
Operator implication: prefix mutations serialise across the cluster (a single global lock); everyday override writes serialise per organisation but run in parallel across organisations.
Async deployment to the IP Mapper¶
Writes commit to PostgreSQL synchronously and return to the API client immediately. Publishing the resulting sessions to the IP Mapper happens asynchronously on a worker pool.
Worker pool and coalescing¶
Each write enqueues the affected org_id for deployment. Workers pull
from the queue, load the organisation's state, compute effective
ranges, group them into sessions, diff against the last-deployed
state, and publish the result.
Multiple writes that arrive for the same organisation while a deploy is already running coalesce into one follow-up deploy. Bursts of writes therefore produce one published diff, not one per write.
Three deploy kinds¶
- Normal (diff). The default. Load the organisation, compute
current sessions, diff against the previously deployed snapshot,
and publish only the differences as
start/update/stopactions. Sessions whose allocations have not changed are not sent. - Full redeploy. Re-publish every current session under a fresh
per-organisation revision, then ask the IP Mapper to clean any
session for this organisation whose revision is older than the
one we just wrote. Triggered by
POST /org/{org}/redeployand by the optional periodic full-redeploy timer. - Delete. Stop every deployed session for the organisation and
ask the IP Mapper to clean the organisation's namespace
subject-wide. Triggered by
DELETE /org/{org}?confirm=trueand by the version scanner when an organisation disappears from PostgreSQL.
Retries¶
Each deploy attempt that fails to publish to the IP Mapper retries with a one-second sleep, up to a fixed budget. After the budget is exhausted the failure is logged and the worker moves on; the next write or the next periodic full-redeploy will pick the organisation back up.
Cleanup contract with the IP Mapper¶
Most session changes ride on start / update / stop actions
in the diff. The separate cleanup command runs in four cases:
- Full redeploy. After publishing every current session under a
fresh revision, orgmanager calls the IP Mapper cleanup command
bounded by
revision_lt, asking it to drop any session for this organisation that predates the redeploy. This is what makes a full redeploy self-healing without a separate "stop everything" step. - First deploy of an organisation in this orgmanager process. After a restart, an orgmanager has no in-memory record of which sessions the IP Mapper currently has for an organisation. The first deploy after the process started — including the deploys the startup version scan queues — therefore runs an unbounded-by-subnet revision cleanup against the IP Mapper, so any sessions left over from a previous orgmanager process are trimmed.
- Prefix removal. After a successful diff publish that included prefix removals, orgmanager calls cleanup bounded by both the organisation and the removed subnets, so unrelated sessions in other prefixes are left untouched.
- Override deletion. Override deletes pass the deleted CIDRs as the cleanup subnet list, and the diff publish that follows ends with a cleanup call bounded by the organisation plus those subnets. The intent is the same as for prefix removal — keep cleanup scoped tightly enough that unrelated sessions stay put.
A cleanup failure is treated as a deploy failure: the deploy attempt
errors out, the worker retries (1 s sleeps, up to the retry budget),
and only after the retry budget runs out does the deploy land in the
error log and bump orgmanager_deploy_errors_total. Even then the
next full-redeploy retries the cleanup, so transient IP Mapper
unavailability heals on its own.
Periodic background work¶
Two timers run in the background, both controlled by the
deploy section.
Version scan¶
Walks every organisation row in PostgreSQL and compares its
org_version against the last deployed version held in memory.
The scan picks up:
- Drift —
org_versionincreased since the last deploy. Queues a normal redeploy. - Discovery — an organisation in PostgreSQL that the orgmanager has not seen before (typically created via the Django admin portal). Queues a deploy.
- Disappearance — an organisation in memory that no longer exists in PostgreSQL. Queues a delete.
Setting version_check_interval: 0 disables the scan. The first
scan also runs at startup to discover existing organisations, and
runs even with the interval set to 0 — that initial scan is what
populates the in-memory cache after a restart.
Periodic full redeploy¶
If enabled, periodically queues a full redeploy for every cached
organisation. This re-asserts every session at the IP Mapper and
trims older revisions, healing any state the IP Mapper may have
missed. A typical operator setting is hours, not minutes — the
diffing path already keeps the IP Mapper in sync, this exists as a
bounded safety net. full_redeploy_interval: 0 disables it.
Storage backends¶
- PostgreSQL is the production target. The DSN is the only
database configuration knob; it accepts the standard
postgres://URL form. - SQLite exists for dev and tests and is selected by prefixing
the DSN with
sqlite:. The production RPM is built without CGO and does not link the SQLite driver — pointingdatabase.dsnat asqlite:URL on a deployed box will refuse to start with a message that names the missing CGO driver. See Configuration → database for the matching DSN reference.