docs: query-pattern for SIP trunk inventory report
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).
This commit is contained in:
parent
90227ab391
commit
2690c2225b
278
docs/query-patterns/sip-trunk-report.md
Normal file
278
docs/query-patterns/sip-trunk-report.md
Normal file
@ -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=<each unique trunk CSS>)` — 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
|
||||||
Loading…
x
Reference in New Issue
Block a user