From 2690c2225bc55d1cf742ac0c7b5a9b63465b3baa Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 25 Apr 2026 23:25:49 -0600 Subject: [PATCH] docs: query-pattern for SIP trunk inventory report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the SQL queries used to build a comprehensive SIP trunk inventory (device + sipdevice + siptrunkdestination joins, plus route_lists_and_groups for membership). Captures rationale for each column, common gotchas (routelistdetail doesn't exist, lvarchar(1) flag fields return 't'/'f' strings), and a draft prompt signature suggesting how to extract this into a @mcp.prompt function in server.py — same shape as the existing route_plan_overview / investigate_pattern / audit_routing prompts. Empty src/mcp_cucm_axl/prompts/ directory remains unused; this lives under docs/ since it's reference material rather than a runtime prompt. Future commit can promote the queries into the prompt function and delete this if redundant. Live result snapshot included for reference (CUCM 15.0.1.12900-234, 2026-04-25, 11 trunks). --- docs/query-patterns/sip-trunk-report.md | 278 ++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 docs/query-patterns/sip-trunk-report.md diff --git a/docs/query-patterns/sip-trunk-report.md b/docs/query-patterns/sip-trunk-report.md new file mode 100644 index 0000000..5ece40e --- /dev/null +++ b/docs/query-patterns/sip-trunk-report.md @@ -0,0 +1,278 @@ +# SIP Trunk Report — Query Pattern + +**Goal:** Produce a comprehensive inventory of every SIP trunk on a CUCM +cluster, with destinations, profile assignments, and downstream +route-group/route-list membership. Useful for handoff documentation, +post-migration cleanup, and identifying single-points-of-failure on +specific trunks. + +**Status:** Validated against CUCM 15.0.1.12900-234 on 2026-04-25. +Empty `prompts/` directory at `src/mcp_cucm_axl/prompts/` is the +intended home for extracting this into a `@mcp.prompt` function. For +now, prompts live inline in `server.py` (see `route_plan_overview`, +`investigate_pattern`, `audit_routing`). + +--- + +## Source-of-truth tables + +| Table | Holds | +|---|---| +| `device` | Trunk row: name, description, FKs to profiles/CSS/pool/location | +| `sipdevice` | SIP-specific config: codec, calling-party selection, RDNIS handling, security, UR I domain | +| `siptrunkdestination` | One row per destination IP/port (a trunk can have multiple, ordered by `sortorder`) | +| `typeclass` | Device class enum — filter `tc.name = 'Trunk'` | +| `sipprofile` | SIP Profile name (joined via `device.fksipprofile`) | +| `callingsearchspace` | CSS name (joined via `device.fkcallingsearchspace`) | +| `devicepool` | Device Pool name (joined via `device.fkdevicepool`) | +| `location` | Location name for CAC/RSVP (joined via `device.fklocation`) | +| `typesipcodec` | Codec name enum (joined via `sipdevice.tksipcodec`) | + +**Not directly relevant but worth knowing:** + +- `sipsecurityprofile` — name lookup for `device.fksecurityprofile`. Skipped in the + query below because the security profile name is rarely informative on a + routine trunk inventory; add the join if security posture matters for the + use case. +- `siptrunkoauth` — additional auth config for OAuth-authenticated trunks. + +--- + +## Query 1 — Trunk inventory (one row per trunk) + +Joins `device` + `sipdevice` and pulls the human-readable names of every FK +field that operators typically want when scanning trunks. + +```sql +SELECT + d.name AS trunk_name, + d.description, + sp.name AS sip_profile, + css.name AS calling_search_space, + dp.name AS device_pool, + loc.name AS location, + tsc.name AS preferred_codec, + sd.requesturidomainname AS sip_domain, + sd.isanonymous AS anon_caller_id, + sd.preferrouteheaderdestination AS prefer_route_header, + sd.acceptinboundrdnis AS accept_inbound_rdnis, + sd.acceptoutboundrdnis AS accept_outbound_rdnis +FROM device d +JOIN typeclass tc ON d.tkclass = tc.enum +JOIN sipdevice sd ON sd.fkdevice = d.pkid +LEFT JOIN sipprofile sp ON d.fksipprofile = sp.pkid +LEFT JOIN callingsearchspace css ON d.fkcallingsearchspace = css.pkid +LEFT JOIN devicepool dp ON d.fkdevicepool = dp.pkid +LEFT JOIN location loc ON d.fklocation = loc.pkid +LEFT JOIN typesipcodec tsc ON sd.tksipcodec = tsc.enum +WHERE tc.name = 'Trunk' +ORDER BY d.name; +``` + +**Why these specific columns:** + +- `description` — operator's free-form annotation; almost always names the + upstream device + IP, useful when the trunk name itself is opaque. +- `sip_profile` — drives transport (UDP/TCP/TLS), early offer, OPTIONS ping, + 100rel, etc. Trunks sharing a SIP profile share *all* of those settings. +- `calling_search_space` — the CSS used when this trunk *originates* a call + (typical for inbound from a SIP carrier hitting the CUCM trunk). +- `device_pool` + `location` — clustering and CAC/RSVP grouping. In a + single-site cluster these are usually homogeneous. +- `preferred_codec` — the codec CUCM advertises first in SDP from this trunk. +- `accept_inbound_rdnis` / `accept_outbound_rdnis` — does the trunk pass RDNIS + (Redirected Dialed Number Identification Service) on diversions/forwards? + Voicemail trunks need both `t`; PSTN-facing trunks usually `f`. + +**LVARCHAR(1) flag fields** (`anon_caller_id`, `prefer_route_header`, +`accept_inbound_rdnis`, `accept_outbound_rdnis`) return `'t'` or `'f'` — not +booleans. Render appropriately in any output. + +--- + +## Query 2 — Destinations (one row per destination IP/port) + +A trunk can have multiple destinations (active/active or +active/standby — sortorder controls retry order). Separate query because of +the one-to-many relationship. + +```sql +SELECT + d.name AS trunk_name, + std.address, + std.port, + std.sortorder +FROM siptrunkdestination std +JOIN sipdevice sd ON std.fksipdevice = sd.pkid +JOIN device d ON sd.fkdevice = d.pkid +ORDER BY d.name, std.sortorder; +``` + +**Notes:** + +- `address` is `VARCHAR(255)` — IP literal *or* DNS name. Expressway-C + trunks often use FQDNs (e.g., `exp-c-p.binghammemorial.org`) so SRV + resolution can shift the actual destination. +- `addressipv6` exists on the same table but is empty on most clusters. +- `port` is `INTEGER` — defaults to 5060 (SIP over UDP/TCP) or 5061 (TLS), + but custom ports are common for non-standard integrations (RightFax, + recording platforms). + +--- + +## Query 3 — Route-group / route-list membership + +**Don't write raw SQL for this** — the relevant join table is +`devicenumplanmap`-adjacent and its name has shifted across CUCM versions. +Use the existing MCP tool: + +``` +route_lists_and_groups() +``` + +Filter the result for `route_groups[].devices[].class == "Trunk"` to get +the set of `(trunk → route group → route list)` triples. Note that some +route lists have route groups with **no static device members** — +those resolve to a Local Route Group via the calling phone's device-pool +`fkroutegroup_local` mapping at call-time (the CUCM Standard Local Route +Group feature). Trunks reachable only through Local Route Groups +won't appear in the static result and require a follow-up call to +`route_device_pool_route_groups()` to enumerate. + +--- + +## Common gotchas + +1. **`routelistdetail` doesn't exist.** I tried it; it fails. The actual + table name varies, and the join logic for route-list → route-group → + device is non-obvious. Use the MCP tool above. +2. **`securityprofile` is `sipsecurityprofile`** for SIP trunks (not the + generic `phonesecurityprofile`). If you add the security profile join, + use the SIP-specific table. +3. **`tkclass` filters by class enum, not text** — but `typeclass.name` + provides the human-readable label. The query above filters on + `tc.name = 'Trunk'` which matches all SIP and ICT trunks. To narrow + to SIP-only, also require `EXISTS (SELECT 1 FROM sipdevice sd WHERE + sd.fkdevice = d.pkid)` (or the inner `JOIN sipdevice` already does that). +4. **Trunks without a primary CSS** are valid — Expressway-C trunks on + this cluster have `fkcallingsearchspace = NULL`. Use `LEFT JOIN` and + render NULL as "(none)" rather than treating it as a finding. + +--- + +## Suggested follow-up tool calls + +After running Query 1+2 and `route_lists_and_groups()`, the audit +narrative usually wants: + +1. `route_devices_using_css(css_name=)` — see what + else uses the same CSS as a particular trunk; helps identify shared + blast-radius dependencies. +2. `route_inspect_pattern(pattern, partition)` — for each route pattern + that targets a trunk-bearing route list, walk the call path. +3. `axl_sql("SELECT name, description FROM sipprofile WHERE pkid IN (...)")` — + if multiple trunks share a SIP profile, look up the profile's full + detail (transport, early-offer, ping, etc.) once. + +--- + +## Findings template (what to call out) + +When this query is wrapped in a `@mcp.prompt`, the prompt should ask the +LLM to surface: + +- **Single-point-of-failure trunks**: any route group with one trunk + member where that route group is the only path for a critical pattern + (911, voicemail, fax). Cross-reference with + `route_lists_and_groups()` device counts. +- **Profile sprawl vs. consolidation**: are 11 trunks using 11 different + SIP profiles, or do most share a small number? Sprawl = harder to + audit transport/timing settings consistently. +- **CSS asymmetry**: are PSTN-facing inbound trunks using a restrictive + CSS that prevents them from reaching internal extensions? Are + internal-facing trunks (voicemail) using a permissive CSS? Mismatches + can cause one-way audio or routing failures. +- **Codec heterogeneity**: most clusters standardize on G.711 µ-law. + Trunks advertising G.722 or G.729 first warrant explanation. +- **DNS-vs-IP destinations**: trunks using FQDNs depend on cluster DNS; + flag if the FQDN resolution path adds a SPOF the audit hadn't + surfaced (e.g., single DNS server). +- **Security posture**: trunks using `Non Secure SIP Trunk Profile` for + carrier-facing connections are a finding worth noting (typical for + premise-equipment SIP carriers, but document the deliberate choice). + +--- + +## Live result snapshot (Bingham, 2026-04-25) + +11 SIP trunks. All on `Main-Campus-Sub1-Pub-DP` except `exp-c-p-SIP-Trk` +(on `Pub-Sub1-DP`). All preferred codec is `711ulaw`. All destinations +on port 5060 (no TLS). + +| Trunk | Destination | SIP Profile | CSS | Location | +|---|---|---|---|---| +| `Forward-Advantage-SIP-Trk` | 172.24.10.10 | FWD Advantage SIP Profile | National-CSS | Main-Campus-LOC | +| `PSTN-Router-SIP-Trk` | 172.20.6.222 | PSTN Sparklite SIP Profile | PSTN-Inbound-CSS | Hub_None | +| `RightFax-SIP-TRK` | 172.20.2.22 | RightFax SIP Profile | FAX-CSS | Hub_None | +| `Unity-Pub-SIP-TRK` | 172.20.6.104 | Unity SIP Profile | Internal-CSS | Main-Campus-LOC | +| `Unity-Sub-SIP-TRK` | 172.20.6.105 | Unity SIP Profile | Internal-CSS | Main-Campus-LOC | +| `VG450-SIP-TRK` | 172.20.6.99 | RightFax SIP Profile | National-CSS | Hub_None | +| `Verba-SIP-TRK` | 172.20.6.120 | Verba Profile | Internal-CSS | Hub_None | +| `ZetaFax-SIP-TRK` | 172.20.14.105 | ZetaFax SIP Profile | FAX-CSS | Hub_None | +| `exp-c-p-SIP-Trk` | exp-c-p.binghammemorial.org | Expressway SIP Profile | (none) | Main-Campus-LOC | +| `exp-c-s-SIP-Trk` | exp-c-s.binghammemorial.org | Expressway SIP Profile | (none) | Main-Campus-LOC | +| `singlewireFusion-SIP-TRK` | 172.20.6.114 | singlewire SIP Profile | (none) | Hub_None | + +**Observations from this snapshot** (templates for what the prompt should +flag): + +- `VG450-SIP-TRK` (analog voice gateway) shares the `RightFax SIP Profile` + with `RightFax-SIP-TRK` — *probably intentional* (both terminate at fax + endpoints) but worth confirming with the operator. +- The 3 trunks with `calling_search_space = NULL` (Expressway-C primary, + Expressway-C secondary, singlewireFusion) all serve specific + device-only paths — they don't originate generic outbound routing. Not + a finding, but a useful invariant to call out. +- `PSTN-Router-SIP-Trk` is the only trunk with `Hub_None` location *and* + `PSTN-Inbound-CSS` *and* a stripped-down CSS — consistent with its role + as the carrier-facing trunk (and as the H1 SPOF in the + [route-plan audit](../../../docs/src/content/docs/audits/2026-04-25-cucm-route-plan.mdx)). + +--- + +## Proposed prompt name and signature + +```python +@mcp.prompt +def sip_trunk_report() -> str: + """Comprehensive SIP trunk inventory: profiles, destinations, + downstream route-group membership, with findings template. + """ + ... +``` + +Or with optional filtering: + +```python +@mcp.prompt +def sip_trunk_report(name_filter: str | None = None) -> str: + """SIP trunk inventory. Pass `name_filter` to narrow to one trunk + (substring match against device.name).""" + ... +``` + +The body should embed the queries above, the follow-up tool-call list, and +the findings template — same pattern as `route_plan_overview`. + +--- + +## Related + +- Existing prompts (inline in `server.py`): `route_plan_overview`, + `investigate_pattern`, `audit_routing` +- Existing tool: `route_lists_and_groups()` — the right way to traverse + the trunk → RG → RL chain +- Existing tool: `route_devices_using_css(css_name)` — for follow-up + blast-radius analysis on each trunk's CSS +- Cisco data dictionary for CUCM 15: search via `cisco-docs` MCP for + "SIPDevice", "SIPTrunkDestination", "Device" tables