24 KiB
Ciena MCMS 6.2 REST API — Implementer's Field Guide
Reference for building an HTTP client against a Ciena MicroClimate
Management System (MCMS) 6.2 PON Manager. Distilled from the official
dev guide (323-1961-306) plus empirical findings against a working
deployment with GNXS / Tibit MicroPlug ONUs on Interos Everest 5.x
firmware.
Language-agnostic — every quirk listed here was hit in production. If you skip the gotchas in §3, you will lose an afternoon to one of them.
1. Connection basics
- Base URL. The root of the PON Manager web UI (e.g.
https://mcms.example.net). All REST paths are prefixed with/api. - Transport. HTTPS. Internal deployments commonly use self-signed certificates on private IPs — your client needs an opt-in to skip TLS verification for that case (do not disable globally).
- HTTP version. HTTP/1.1 works. The server also negotiates HTTP/2 but the dev-guide examples are 1.1 and the only behavioural difference observed is irrelevant to clients.
- Path normalisation. The dev guide writes paths as
/v1/users/authenticate/— the real wire path is/api/v1/users/authenticate/. Normalise once in your client. - API versions. Both
/api/v1/...and/api/v3/...exist and serve different endpoints. Use whichever the dev guide lists for that resource — they are not interchangeable. See §4 endpoint table. - Trailing slashes. Required.
/api/v1/onus/configs/<id>/works;/api/v1/onus/configs/<id>does not (Django'sAPPEND_SLASHis on, but a few endpoints 500 instead of redirecting).
2. Login & session
MCMS uses Django session cookies + a CSRF token. Sessions do not auto-expire on idle — log out explicitly when done.
2.1 Authenticate
POST /api/v1/users/authenticate/
Content-Type: application/json
{ "data": { "email": "user@example.net", "password": "..." } }
Response on success (HTTP 200):
{ "status": "success" }
Set-Cookie headers carry the session. On observed MCMS 6.2 builds:
Set-Cookie: __Host-csrftoken=<token>; Path=/; Secure; SameSite=Lax
Set-Cookie: __Host-sessionid=<id>; Path=/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: __Host-sessionexpire=<epoch>; Path=/; Secure; SameSite=Lax
→ The __Host- prefix is significant. Some MCMS builds (older
ones, mostly) use bare csrftoken / sessionid. Your cookie reader
should accept either prefix.
→ Some build variants accept an unwrapped body too:
{"email": ..., "password": ...} with no data envelope. If wrapped
returns HTTP 400, retry unwrapped.
2.2 Subsequent requests
Every authenticated request must send:
Cookie: __Host-csrftoken=<token>; __Host-sessionid=<id>; ...
X-CSRFToken: <token> # the value from __Host-csrftoken
Referer: <baseURL>/ # MCMS rejects POST/PUT/DELETE without this
User-Agent: <anything> # see §3.1 — must not be empty
Accept: application/json
For PUT/POST/PATCH also:
Content-Type: application/json
Content-Length: <bytes>
2.3 (Optional) Database selection
If the deployment manages multiple databases:
PUT /api/v1/databases/selection/
{ "data": "<database_id>" }
Most deployments only have one and skip this step.
2.4 Logout
GET /api/v1/users/logout/
Best-effort — don't fail your client if this errors.
3. The gotchas (read this section twice)
3.1 User-Agent header is REQUIRED
If you don't send a User-Agent, some MCMS middleware dereferences
HTTP_USER_AGENT without a null check and returns HTTP 500 with
body:
{"status": "fail", "details": {"message": "Internal server error. See server logs for details."}}
Reproduce: curl -A '' https://mcms/api/v3/onus/configs/. Any non-empty
string works (PonFleetClient/1.0, etc.).
For iOS/Swift: URLSession sets a default User-Agent automatically
(e.g. <bundle>/1 CFNetwork/... Darwin/...), so this is unlikely to
bite you there — but if you ever set a custom URLRequest with
setValue(nil, forHTTPHeaderField: "User-Agent"), the 500s will
return.
3.2 CSRF token field name
The token cookie is __Host-csrftoken on new builds, bare csrftoken
on older. The header you send back is always X-CSRFToken (no
prefix, capital T in Token). Both Django case-fold the header
name, so casing is fine — but the body name is exact.
3.3 Pagination: use next, NOT skip
/api/v3/onus/configs/?skip=N has been observed returning HTTP 500.
The working approach uses the last document's _id as a cursor:
GET /api/v3/onus/configs/?limit=100
→ returns 100 docs, the last with _id = "X"
GET /api/v3/onus/configs/?limit=100&next=X
→ returns the next 100, EXCLUDING all _ids <= X
Loop until you get a short page (< limit) or an empty page. Most list endpoints accept this pattern.
3.4 Page size 100 is the safe default
?limit=1000 against /api/v3/onus/configs/ on a ~1500-ONU fleet
times out at the Apache front-end before Django logs the request.
Full ONU-CFG documents are ~30 KB each, so 1000 × 30 KB = 30 MB,
which exceeds the proxy timeout. 100 per page keeps each response
~3 MB and well under any default proxy budget.
This applies to all bulk list endpoints (configs, states, OLT states).
3.5 Request body envelope: {"data": payload}
PUT/POST/PATCH bodies are wrapped:
{ "data": { "_id": "...", "ONU": { ... } } }
Response envelope is similar:
{ "status": "success", "data": { ... } }
{ "status": "fail", "details": { "message": "..." } }
{ "status": "warning", "data": { ... }, "details": { ... } }
"warning" is treated as success by MCMS (the write happened) but
indicates a schema mismatch you should log.
3.6 PUT is full-document replace, NOT patch
PUT /api/v1/onus/configs/<id>/ replaces the entire ONU-CFG
document. You must GET the current doc, mutate fields, and PUT
the entire result. There is no PATCH equivalent that works reliably
on MCMS 6.2.
Forgetting this turns into "I PUT'd just the FW Bank fields and now the ONU has no service config" — the dev guide is explicit but easy to skim past.
3.7 Timestamp format
MCMS emits and accepts timestamps as:
"YYYY-MM-DD HH:MM:SS" # second precision
"YYYY-MM-DD HH:MM:SS.ffffff" # microsecond precision (for STATE.Time)
Always UTC, always with a literal space between date and time
(not ISO-8601 T). Used on:
ONU-STATE.TimeOLT-STATE.Time- Alarm timestamps
AUTO-TASK-CFG.Task["Scheduled Start Time"]start-time/end-timeURL params on stats endpoints
For Swift: ISO8601DateFormatter will NOT parse this. Use a
plain DateFormatter with:
let fmt = DateFormatter()
fmt.timeZone = TimeZone(identifier: "UTC")
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS" // or "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
The en_US_POSIX locale matters — without it, devices set to a
24-hour-format locale will still parse fine but devices set to
12-hour-format locales will fail. POSIX locale forces parse rules.
3.8 Server-side query/projection filters are brittle
The dev guide documents query, projection, sort, limit,
skip, next, distinct URL parameters. In practice:
query(Mongo-style filter) works for simple equality on bare field names. Anything with spaces in field names, quoted values with non-ASCII characters, or projection paths that don't exist on every document version is unreliable — you'll get either empty results or 400s.projectionis very picky about paths. Better to fetch full docs and project client-side.
Recommended approach: load the full fleet (paginated by next)
and filter client-side. The data volume is fine for any device with
enough RAM to hold a few thousand 30 KB documents.
3.9 URL encoding
Use percent-encoding (encodeURIComponent equivalent). MCMS does NOT
accept + for spaces in URL parameters — use %20.
This matters for query params like start-time which carry literal
spaces in the timestamp.
3.10 ONU IDs
The _id field of an ONU document is:
- The serial number for XGS-PON / GPON ONUs (e.g.
GNXS05057fee) - The MAC address for 10G-EPON ONUs
Treat as opaque strings. They're URL-safe in observed deployments
but always encodeURIComponent to be defensive.
3.11 Registration status lives on OLT-STATE, not ONU-STATE
This is non-obvious. The 9 registration buckets live as arrays on the OLT's state document:
{
"_id": "<OLT MAC>",
"ONU States": {
"Registered": ["GNXS01", "GNXS02", ...],
"Deregistered": ["GNXS09", ...],
"Dying Gasp": [...],
"Disabled": [...],
"Disallowed Admin": [...],
"Disallowed Error": [...],
"Disallowed Reg ID": [...],
"Unspecified": [...],
"Unprovisioned": [...]
}
}
To resolve "what's the status of ONU X?", you walk every OLT-STATE doc, find which bucket contains X, and that's your answer. See Procedure 2 in the dev guide.
An ONU should appear in exactly one bucket on exactly one OLT. In the
rare duplicate case, prefer the non-Registered bucket — that's the
interesting signal.
3.12 FEC counters live on ONU-STATE.STATS, not on /onus/stats/
GET /api/v1/onus/stats/<id>/?start-time=... returns historical PM
samples (time-series). For the current FEC counters and optical
levels, read the live ONU-STATE document:
GET /api/v3/onus/states/<id>/
→ data.STATS["ONU-PON"]["RX Pre-FEC BER"]
→ data.STATS["ONU-PON"]["RX Post-FEC BER"]
→ data.STATS["ONU-PON"]["RX Optical Level"] (dBm)
→ data.STATS["ONU-PON"]["TX Optical Level"] (dBm)
→ data.STATS["OLT-PON"]["RX Pre-FEC BER"]
→ data.STATS["OLT-PON"]["RX Post-FEC BER"]
The MCMS Web UI's /api/onu/summary helper returns the same data
under data.state_collection.STATS.* (wrapper shape).
3.13 Bulk endpoints don't return everything in one shot
Even on a small fleet, MCMS may silently truncate large list
responses. Always paginate — don't trust ?limit=large_number to be
authoritative.
3.14 Verbose error logging matters
When MCMS 500s, the response body sometimes contains useful detail (missing required param, schema validation failure, etc.). Always log both the response headers and the FULL body on non-2xx during development — most "mysterious 500" cases resolve to a one-line hint in the body.
4. Endpoints used by the fleet-upgrade app
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/users/authenticate/ |
Login |
GET |
/api/v1/users/logout/ |
Logout |
PUT |
/api/v1/databases/selection/ |
(Optional) DB selection |
GET |
/api/v3/onus/configs/ |
List ONU configs (paginated) |
GET |
/api/v1/onus/configs/<id>/ |
Get single ONU config |
PUT |
/api/v1/onus/configs/<id>/ |
Replace ONU config (full doc) |
DELETE |
/api/v1/onus/configs/<id>/ |
Delete ONU (Procedure 43) |
GET |
/api/v3/onus/states/ |
List ONU states (paginated) |
GET |
/api/v3/onus/states/<id>/ |
Get single ONU state |
DELETE |
/api/v1/onus/states/<id>/ |
Delete ONU state (best-effort) |
GET |
/api/v3/olts/states/ |
List OLT states (paginated; registration) |
GET |
/api/v1/files/onu-firmware/ |
List uploaded firmware files |
POST |
/api/v1/files/onu-firmware/<name>/ |
Upload firmware |
PUT |
/api/v3/tasks/configs/<id>/ |
Create bulk upgrade task (Procedure 8) |
GET |
/api/v3/tasks/configs/<id>/ |
Poll bulk task config |
GET |
/api/v1/onus/<id>/upgrade/status/ |
(Build-dependent) per-ONU status |
Other endpoints documented but not (yet) used
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/onus/stats/<id>/?start-time=... |
Time-series ONU PM data |
GET |
/api/v1/olts/stats/<id>/?start-time=... |
OLT PM data |
GET |
/api/v1/onus/logs/<id>/?start-time=... |
Per-ONU syslog |
GET |
/api/v1/olts/logs/<id>/?start-time=... |
Per-OLT syslog |
GET |
/api/v3/tasks/states/<id>/ |
Bulk task progress (good follow-up) |
GET |
/api/v1/onus/automation/states/ |
PON automation state |
GET |
/api/v1/onus/cpe-states/ |
ONU CPE states (downstream devices) |
5. Data shapes (observed)
These are not formally specified in the dev guide for MCMS 6.2 — they were extracted from live traffic.
5.1 ONU-CFG
{
"_id": "GNXS05057fee",
"ONU": {
"Name": "Operator-set label",
"Address": "Street, city",
"PON Mode": "GPON", // GPON | XGS-PON | 10G-EPON
"Equipment ID": "FT-XGS2110",
"FW Bank Files": ["...bin", "...bin"], // [slot 0, slot 1]
"FW Bank Versions": ["EV05110R", "EV05100R"],
"FW Bank Ptr": 0, // 0 | 1 | 65535 (unset)
"ONU-ALARM-CFG": "Default",
"Vendor": "GNXS",
"Serial Number": "GNXS05057fee",
"Host MAC Address": "58:00:32:05:7f:ee",
// ...many more fields - preserve all of them when PUTing
},
"OLT-Service 0": { ... }, // service config slots
"OLT-Service 1": { ... },
// ...
}
Critical: when mutating for an upgrade, only change ONU["FW Bank Files"], ONU["FW Bank Versions"], and ONU["FW Bank Ptr"]. Pass
through everything else unchanged.
5.2 ONU-STATE
{
"_id": "GNXS05057fee",
"Time": "2026-05-04 21:51:57.394127", // UTC, last update
"ONU": {
"Equipment ID": "FT-XGS2110",
"FW Bank Files": [...],
"FW Bank Versions": [...],
"FW Bank Ptr": 0,
"FW Version": "EV05110R", // resolved active version
"FW Upgrade Status": {
"Bank": 0,
"Status": "Success", // status string
"Progress": 100, // percent
"Upgrade Time": "2026-03-18 17:12:43.093893",
"Total Blocks": 482915,
"Sent Blocks": 482915,
"Failures": 0,
// ...
},
"PON Mode": "GPON",
"Online Time": "2026-03-18 17:13:42.285965",
// ...
},
"STATS": {
"ONU-PON": {
"RX Pre-FEC BER": 22388041291, // cumulative since reset
"RX Post-FEC BER": 22379111198,
"RX Optical Level": -17.544, // dBm
"TX Optical Level": 5.532 // dBm
},
"OLT-PON": {
"RX Pre-FEC BER": 0, // upstream view
"RX Post-FEC BER": 0,
"TX Optical Level": 5.6,
"RX Optical Level": -18.3
},
"ONU-EnhancedFecPmHistData-32769": { // NOT the same as the BER fields above
"uncorrectable_code_words": 1829, // (these are codeword counts,
"corrected_bytes": 181, // not bit-error-rate ratios)
"corrected_code_words": 181
},
// ...many more
},
"Alarm": {
"0-EMERG": [], "1-ALERT": [], "2-CRIT": [],
"3-ERROR": [], "4-WARNING": [], "5-NOTICE": [],
"6-INFO": [], "7-DEBUG": []
}
}
5.3 OLT-STATE (relevant subset)
{
"_id": "<OLT MAC>",
"Time": "2026-05-04 21:51:57.394127",
"ONU States": {
"Registered": ["onu1", "onu2", ...],
"Deregistered": [...],
"Dying Gasp": [...],
"Disabled": [...],
"Disallowed Admin": [...],
"Disallowed Error": [...],
"Disallowed Reg ID": [...],
"Unspecified": [...],
"Unprovisioned": [...]
}
// ...
}
5.4 AUTO-TASK-CFG (bulk firmware task, Procedure 8)
{
"_id": "<your-task-id>", // unique, caller-generated
"Task": {
"Device Type": "ONU",
"Operation": "Firmware Upgrade",
"Scheduled Start Time": "2026-05-12 03:00:00" // UTC space format
},
"Task Details": {
"Devices": ["GNXS01", "GNXS02", ...], // ONU IDs (serial / MAC)
"ONU": {
"FW Bank Ptr": 1, // target slot (single value!)
"FW Bank Files": { "0": "old.bin", "1": "new.bin" },
"FW Bank Versions": { "0": "EV05110R", "1": "EV05120R" }
}
}
}
PUT to /api/v3/tasks/configs/<task-id>/. Note: FW Bank Files and
FW Bank Versions here are objects keyed by slot index as strings
("0", "1"), not arrays. This is different from the ONU-CFG
shape where the same fields are arrays — almost certainly a Mongo
serialisation quirk. Match it as-is.
Single bank per task. Because FW Bank Ptr accepts one slot, if
your selection spans ONUs with different active banks, bucket them
by target slot and create one task per bucket.
5.5 Firmware file entry (from /api/v1/files/onu-firmware/)
{
"_id": "...",
"filename": "Interos-Everest-5.12.0-R-EV05120R.bin",
"metadata": {
"Compatible Manufacturer": "TIBITCOM",
"Compatible Model": ["MicroPlug ONU"],
"Version": "EV05120R"
},
// GridFS metadata...
}
To upload:
POST /api/v1/files/onu-firmware/<filename>/
{
"data": {
"file": "<base64 binary contents>",
"metadata": {
"Compatible Manufacturer": "TIBITCOM",
"Compatible Model": ["MicroPlug ONU"],
"Version": "EV05120R"
}
}
}
6. Firmware upgrade flow
Two methods documented:
6.1 Procedure 7 — per-ONU PUT
For each ONU:
GET /api/v1/onus/configs/<id>/→ full doc.- Compute the inactive bank (see §6.3).
- Set:
ONU["FW Bank Files"][writeSlot] = "<filename>"ONU["FW Bank Versions"][writeSlot] = "<version>"ONU["FW Bank Ptr"] = writeSlot(this initiates the download)
PUT /api/v1/onus/configs/<id>/with the entire mutated document.
Pros: see errors per ONU live. Cons: slow for large fleets, you own retries.
6.2 Procedure 8 — bulk task (recommended)
Build one AUTO-TASK-CFG per (target slot) bucket:
PUT /api/v3/tasks/configs/<task-id>/
(body shape in §5.4)
MCMS owns the rollout: scheduled start, retries, pacing. Monitor progress (build permitting):
GET /api/v3/tasks/states/<task-id>/
Or just poll the ONU configs and watch FW Bank Versions /
FW Bank Ptr flip.
6.3 Inactive-bank selection
FW Bank Ptr identifies the active bank (0, 1, or 65535 = unset).
Always write to the inactive bank so you don't blow away the
running image:
Current FW Bank Ptr |
Write slot | Why |
|---|---|---|
| 0 | 1 | preserve active |
| 1 | 0 | preserve active |
| 65535 (unset) | 1 | matches Procedure 7 example; slot 0 stays as factory fallback |
MCMS flips FW Bank Ptr to the new slot when the download completes.
The ONU reboots into the new image on its next reboot.
7. FEC + optical interpretation
7.1 Four FEC counters
Per the user guide green-LED criteria (323-1961-302 p149):
| Field | Path | Green | Red |
|---|---|---|---|
| ONU Pre-FEC BER | STATS["ONU-PON"]["RX Pre-FEC BER"] |
0 | (informational) |
| ONU Post-FEC BER | STATS["ONU-PON"]["RX Post-FEC BER"] |
0 | > 0 |
| OLT Pre-FEC BER | STATS["OLT-PON"]["RX Pre-FEC BER"] |
0 | (informational) |
| OLT Post-FEC BER | STATS["OLT-PON"]["RX Post-FEC BER"] |
0 | > 0 |
Note: the field name is "BER" but the magnitudes are big integers (cumulative error byte counts since reset), not ratios in [0,1]. Treat them as counts.
7.2 Health bucket rules
Suggested 4-state reduction:
- OK: all four counters present and == 0.
- Pre-FEC errors: any pre > 0, all post == 0. (Marginal link; FEC is correcting.)
- Post-FEC errors: any post > 0. (Uncorrected bits reaching the user.)
- Buggy (pre ≈ post): ONU pre > 0 AND post > 0 AND
post / pre >= 0.95. Real FEC reduces errors; if post is within 5% of pre, the firmware is mirroring the counter. Observed on Interos Everest ONUs.
7.3 Optical levels
STATS["ONU-PON"]["RX Optical Level"] // ONU receive power, dBm
STATS["ONU-PON"]["TX Optical Level"] // ONU transmit power, dBm
User guide thresholds:
| Green | Red | |
|---|---|---|
| RX | ≥ −30 dBm | < −30 dBm |
| TX | ≥ 3 dBm | < 3 dBm |
Operators usually want a margin warning band — e.g. yellow when RX is between −28 and −30 dBm.
8. iOS-specific notes
8.1 Library stack suggestions
- HTTP:
URLSessionis fine. The features you need are standard cookie storage, custom headers, and the option to bypass cert validation. Don't reach for Alamofire just for this. - Cookies:
HTTPCookieStoragehandles__Host-prefixes correctly. Get the CSRF value via:HTTPCookieStorage.shared.cookies(for: baseURL)? .first { $0.name == "__Host-csrftoken" || $0.name == "csrftoken" }?.value - Self-signed certs: Implement
URLSessionDelegate.urlSession(_:didReceive:completionHandler:)and acceptserverTrustonly when the user has explicitly toggled "accept self-signed" for the configured host. Never default-on. - JSON:
JSONSerializationis simpler thanCodablefor the envelope pattern ({status, data, details}) because thedatapayload is heterogeneous.Codableworks if you model each endpoint individually.
8.2 Concurrency
The fleet load is N HTTPS round-trips for per-ONU state (the bulk
endpoint is the optimisation but the per-ID fallback may also be
needed). Use Swift Concurrency (TaskGroup) with a concurrency cap
of 8–10 to avoid stampeding the server. The desktop client uses 5
for deletes and 8 for state fetches.
8.3 Background uploads
Firmware .bin files are 10–50 MB base64-encoded. If you implement
uploads:
- Don't block the main thread.
- Consider
URLSessionbackground configuration so an upload can survive an app suspend. - The upload is a single POST; no chunking required.
8.4 Storing the session
For UX: persist the base URL and username in UserDefaults, never
the password. Re-prompt for password on launch.
The session cookie itself goes in the per-session cookie storage and is automatically discarded — don't try to persist it.
8.5 Date handling
See §3.7. Default DateFormatter with en_US_POSIX locale + UTC
timezone, format "yyyy-MM-dd HH:mm:ss.SSSSSS" (microseconds) or
"yyyy-MM-dd HH:mm:ss" (seconds). Try the microsecond format
first, fall back to second if it fails.
8.6 Cell-vs-Wi-Fi
MCMS APIs are typically reachable only from the operator's internal network. Test on cellular: many internal hosts won't be reachable, and your error UI needs to communicate that clearly.
8.7 Self-signed cert prompt
Make this a first-class onboarding step. The lab MCMS at
https://mcms.lab.svorka.net/ uses a self-signed cert, and any
auto-rejection will silently break login.
9. Procedures referenced in the dev guide
| # | Title | Notes |
|---|---|---|
| 2 | Determining ONU registration status | OLT-STATE["ONU States"] buckets |
| 7 | Per-ONU firmware upgrade | Full-doc PUT to ONU-CFG |
| 8 | Bulk firmware upgrade | AUTO-TASK-CFG via PUT |
| 38 | Resetting bit error rate values | Names the four FEC fields |
| 43 | Deleting an ONU | DELETE on ONU-CFG |
(Page numbers vary by document revision; cite by procedure number.)
10. Quick implementation checklist for a new client
A correct first POST-login GET against /api/v3/onus/configs/?limit=1
returning HTTP 200 means you've got the eight critical pieces right:
- HTTPS reachable (with cert exception if needed)
- Path prefix
/apiadded - User-Agent set to non-empty value
- Cookie jar storing
__Host-sessionid+__Host-csrftoken X-CSRFTokenheader pulled from__Host-csrftokencookie valueRefererheader set to base URLAccept: application/json- Trailing slash on the path
Add for write operations:
Content-Type: application/json- Body wrapped:
{"data": ...} - Full-document PUT (never partial)
Add for list endpoints:
- Page size ≤ 100
nextcursor (notskip)- Loop until short page
Once those land, the API is comfortable to work with — the hard parts are upfront.
Compiled from 323-1961-306 (REST API dev guide), 323-1961-302
(PON Manager User Guide), and empirical testing against MCMS 6.2 with
Interos Everest 5.11/5.12 firmware on GNXS / Tibit MicroPlug ONUs.