Add Outbound Routes REST API (#686)#713
Open
Diverious wants to merge 1 commit into
Open
Conversation
Outbound routes (rows in dr_rules with groupid == FLT_OUTBOUND or in
[FLT_LCR_MIN, FLT_FWD_MIN)) were only manageable via the Flask GUI at
gui/dsiprouter.py:1710-1932; this adds /api/v1/outboundroutes with full
GUI parity, including LCR (from-prefix) routing. Any route createable
in the GUI is now createable via the API and vice versa, unblocking
automation, scripting, and customer integration use cases.
New module
----------
gui/modules/api/outboundroutes/
__init__.py package marker
functions.py business logic with no Flask deps, callable
independently for unit-style testing:
validateOutboundRouteBody, nextLcrGroupid,
serializeOutboundRoute, createOutboundRoute,
updateOutboundRoute, deleteOutboundRoute
routes.py handleOutboundRoutes Flask handler for
GET/POST/PUT/DELETE on both /api/v1/outboundroutes
and /api/v1/outboundroutes/<int:ruleid>; gated by
@api_security and carrying a full Sphinx httpdomain
docstring with Request/Response Payload examples
Wired into gui/dsiprouter.py with the same idiom the licensemanager,
carriergroups, and auth blueprints use: import, app.register_blueprint,
csrf.exempt -- three single-line additions.
API surface
-----------
GET /api/v1/outboundroutes list (simple + LCR)
GET /api/v1/outboundroutes/<ruleid> get one (also ?ruleid=N)
POST /api/v1/outboundroutes create
PUT /api/v1/outboundroutes/<ruleid> update (also ?ruleid=N)
DELETE /api/v1/outboundroutes/<ruleid> delete (also ?ruleid=N)
Path-style URLs for single-record routes match carriergroups and
endpointgroups; ?ruleid=N is also accepted for symmetry with the
existing inboundmapping API idiom. PUT and DELETE are also declared on
the no-path-arg URL so the handler's "ruleid is required" 400 path is
reachable -- otherwise Flask would short-circuit with 405 before the
handler ran.
Request body fields (all optional except gwgroupid on POST):
name friendly name (stored 'name:<value>' in description)
from_prefix presence promotes the rule to LCR routing
prefix To-prefix (required when from_prefix is set, mirrors
the GUI JS validator at
gui/static/js/outboundroutes.js:19-28)
timerec Kamailio time-recurrence pattern
priority int, default 0
routeid custom Kamailio route name
gwgroupid carrier group id; required on POST, '0' rejected
(matches the GUI at gui/dsiprouter.py:1740 which
filters '0' to mean 'no carrier group')
Response uses the standard createApiResponse envelope
{error, msg, kamreload, dsipreload, data, status_code}. Every mutation
flips shared-memory kam_reload_required so the reload daemon fires
drouting.reload on the next tick. INFO/ERROR events go through the IO
class (gui/shared.py:164+) the same way every other API handler logs.
LCR (Least Cost Routing) semantics
----------------------------------
Mirrors addUpateOutboundRoutes (gui/dsiprouter.py:1710-1875) exactly:
No from_prefix:
groupid = FLT_OUTBOUND (8000), no paired dsip_lcr row.
POST with from_prefix:
groupid auto-allocated to max(dr_groupid in
[FLT_LCR_MIN, FLT_FWD_MIN)) + 1, or FLT_LCR_MIN if none exist;
paired dsip_lcr row inserted with
pattern = '<from_prefix>-<prefix>'.
PUT promote (set from_prefix on a simple route):
new groupid in LCR range, new dsip_lcr row.
PUT demote (clear from_prefix on an LCR route):
groupid reset to FLT_OUTBOUND, paired dsip_lcr row deleted.
PUT in-place (change from_prefix on an LCR route):
dsip_lcr pattern + from_prefix updated, groupid unchanged.
DELETE:
paired dsip_lcr rows matched by dr_groupid removed first, then
the dr_rules row.
The PUT handler fetches the existing row before deciding which branch
to take, which sidesteps the GUI's known limitation at
gui/dsiprouter.py:1845 ("not sure how to correlate stale LCR entries
without old from-prefix") by always knowing the current groupid and
from_prefix before mutating.
gwlist is serialized as '#<gwgroupid>' (gateway-group reference) to
match the GUI's storage format.
Testing
-------
testing/19.sh extended with 29 integration cases covering every code
path: every HTTP method, every status code (200/400/404/401), every
validator branch, both ID conventions (path-style and ?ruleid=), all
four LCR update branches, kamreload-flag verification, and DB-state
asserts after each mutation via mysql SELECT. Assertions use exact
codes (-ne <expected>) instead of the weaker -ne 200 idiom in the
existing inboundmapping tests, so an unexpected 500 fails loudly.
cleanupHandler() extended with description and from_prefix LIKE
filters scoped to test-row markers ('name:Test Outbound %' and
'999%') so production LCR rules are never touched.
testing/manual/outboundroutes_smoke.py exercises the helper layer
directly (no HTTP) against the live DB. Useful for diagnosing whether
a failure is in the routing layer or the business logic; names use
the same 'Test Outbound %' / '999%' markers so any rows leaked on
failure are swept by the next testing/19.sh run.
Verified locally end-to-end against a containerized MySQL with the
kamailio schema: 67/67 assertions passing across helper-layer (41)
and HTTP-layer (26) harnesses. The 'gwgroupid="0"' validation case
caught a real Python truthiness bug ('not "0"' is False because the
string is non-empty) that an earlier draft would have shipped.
Postman
-------
testing/api/dsiprouter.postman_collection.json grows a new
outboundroutes folder with six requests: list, get-by-id, POST simple,
POST LCR, PUT, DELETE -- each with example bodies that exercise the
canonical request shape.
Documentation
-------------
docs/source/dev/modules.rst automodule entries for
routes and functions
docs/source/user/api.rst curl examples (list /
create-simple /
create-LCR / update /
delete)
docs/source/user/global_outbound_routes.rst cross-link from the GUI
walkthrough to the API
user page
The blueprint docstring is auto-extracted into Sphinx
routes/summary.rst and routes/details.rst via the existing
.. qrefflask:: and .. autoflask:: directives at
docs/source/conf.py:46-49, so the public API reference picks the
endpoint up with no additional Sphinx configuration.
Known limitations (match GUI behavior; out of scope for this PR)
----------------------------------------------------------------
* nextLcrGroupid concurrency: two simultaneous LCR POSTs could be
assigned the same groupid (no SELECT ... FOR UPDATE). Same race
exists in the GUI handler at gui/dsiprouter.py:1754-1762 and
1811-1818.
* description codec: name fields containing ',' or ':' would break
the key:value,key:value description encoding. GUI has no
validation here either.
* gwgroupid existence is not validated against dr_gw_lists. Matches
the inboundmapping API's same TODO at
gui/modules/api/api_routes.py:565.
* Carrier-group display name is not returned in the response; only
gwgroupid. The GUI joins dr_gw_lists for the friendly name; the
API returns just the id.
* dsip_hardfwd / dsip_failfwd attachment to outbound routes is not
exposed, because the GUI's /outboundroutes page doesn't expose it
either.
Closes dOpensource#686
Collaborator
|
Can one of the admins verify this patch? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Closes #686.
Outbound routes (rows in
dr_ruleswithgroupid == FLT_OUTBOUNDor in[FLT_LCR_MIN, FLT_FWD_MIN)) were only manageable via the Flask GUI atgui/dsiprouter.py:1710-1932. This PR adds/api/v1/outboundrouteswith full GUI parity, including LCR (from-prefix) routing, so any route createable in the GUI is now createable via the API and vice versa — unblocking automation, scripting, and customer integration use cases.API surface
/api/v1/outboundroutes/api/v1/outboundroutes/<ruleid>?ruleid=N)/api/v1/outboundroutes/api/v1/outboundroutes/<ruleid>?ruleid=N)/api/v1/outboundroutes/<ruleid>?ruleid=N)Path-style URLs for single-record routes match
carriergroupsandendpointgroups; query-string?ruleid=Nis also accepted for symmetry with the existinginboundmappingAPI. PUT and DELETE are declared on the no-path-arg URL too, so the handler's "ruleid is required" 400 path is reachable (otherwise Flask short-circuits with 405 before the handler runs).Request body fields (all optional except
gwgroupidon POST):namename:<value>indescription)from_prefixprefixfrom_prefixis set, mirrors the GUI JS validator atgui/static/js/outboundroutes.js:19-28)timerecpriorityrouteidgwgroupid"0"rejected (matches GUI atgui/dsiprouter.py:1740)Response uses the standard
createApiResponseenvelope{error, msg, kamreload, dsipreload, data}. Every mutation flips shared-memorykam_reload_requiredso the reload daemon firesdrouting.reloadon the next tick. INFO/ERROR events go through theIOclass (gui/shared.py:164+) like every other API handler.LCR semantics
Mirrors
addUpateOutboundRoutes(gui/dsiprouter.py:1710-1875) exactly:from_prefixgroupid = FLT_OUTBOUND(8000), no paireddsip_lcrrowfrom_prefixgroupidauto-allocated tomax(dr_groupid in [FLT_LCR_MIN, FLT_FWD_MIN)) + 1(orFLT_LCR_MINif none); paireddsip_lcrrow withpattern = '<from_prefix>-<prefix>'from_prefixset on simple route) → new groupid in LCR range, newdsip_lcrrowfrom_prefixon LCR route) → groupid reset toFLT_OUTBOUND, paireddsip_lcrrow deletedfrom_prefixon LCR route) →dsip_lcrpattern +from_prefixupdated, groupid unchangeddsip_lcrrows removed first, then thedr_rulesrowThe PUT handler fetches the existing row before deciding the branch, which sidesteps the GUI's known TODO at
gui/dsiprouter.py:1845("not sure how to correlate stale LCR entries without old from-prefix").gwlistis serialized as'#<gwgroupid>'to match the GUI's storage format.Type of change
How Has This Been Tested?
Automated coverage
testing/19.shextended with 29 integration cases covering every code path: every HTTP method, every status code (200 / 400 / 404 / 401), every validator branch, both ID conventions (path-style and?ruleid=), all four LCR update branches, kamreload-flag verification, and DB-state asserts after each mutation viamysql SELECT. Assertions use exact-code comparisons (-ne <expected>) instead of the weaker-ne 200idiom in the existing inboundmapping tests, so an unexpected 500 fails loudly.cleanupHandler()extended withdescription LIKE 'name:Test Outbound %'andfrom_prefix LIKE '999%'filters so production LCR rules are never touched.testing/manual/outboundroutes_smoke.pyexercises the helper layer directly (no HTTP) against the live DB; useful for diagnosing whether a failure is in the routing layer or the business logic.Verified locally
End-to-end against a containerized MySQL with the kamailio schema: 67/67 assertions passing across helper-layer (41) and HTTP-layer (26) harnesses. The
gwgroupid="0"validation case caught a real Python truthiness bug (not "0"is False because the string is non-empty) that an earlier draft would have shipped.Reproducing
Test Configuration
drouting.reloadverificationDocumentation
docs/source/dev/modules.rstoutboundroutes.routesandoutboundroutes.functionsdocs/source/user/api.rstdocs/source/user/global_outbound_routes.rstThe Flask blueprint docstring is auto-extracted into
routes/summary.rstandroutes/details.rstvia the existing.. qrefflask::and.. autoflask::directives indocs/source/conf.py:46-49, so the public API reference picks the endpoint up with no additional Sphinx configuration.Postman collection (
testing/api/dsiprouter.postman_collection.json) gains a newoutboundroutesfolder with six requests (list, get-by-id, POST simple, POST LCR, PUT, DELETE).Known limitations (match GUI behavior; out of scope for this PR)
nextLcrGroupidconcurrency: two simultaneous LCR POSTs could be assigned the same groupid (noSELECT ... FOR UPDATE). Same race exists in the GUI atgui/dsiprouter.py:1754-1762, 1811-1818.descriptioncodec:namefields containing,or:would break thekey:value,key:valueencoding. GUI has no validation here either.gwgroupidexistence not validated againstdr_gw_lists— matches the inboundmapping API's same TODO atgui/modules/api/api_routes.py:565.gwgroupid. The GUI joinsdr_gw_listsfor the friendly name; the API returns just the id.dsip_hardfwd/dsip_failfwdattachment not exposed — the GUI's/outboundroutespage doesn't expose it either.Checklist