Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cfwd — Call Forwarding Automation for Cisco Unified CM

A validated, auditable layer in front of repetitive call-forwarding changes on Cisco Unified Communications Manager (CUCM). The desired state lives in a reviewed input file, not in an individual's memory. Every change is validated before it runs, planned as a diff, applied idempotently, verified, and captured for rollback.

This is a reference implementation of the pattern described in the article Automating Call Forwarding Management at Enterprise Scale. It ships with an offline mock CUCM so you can run the full workflow — validation, dry-run, apply, verify, rollback — without a live cluster.

Why

The CUCM administration GUI will happily accept a forward whose Calling Search Space cannot reach the destination. The change saves successfully and the routing failure only appears when a caller hits it. cfwd moves those checks before the change, and makes every change reviewable and reversible.

Features

  • Validation first — DN existence, destination existence, CSS reachability, forwarding-loop detection, and required change metadata (reason + ticket).
  • Dry-run by default — the plan is a current → desired diff. Nothing is written unless you pass --apply.
  • Idempotent apply — a line already in the desired state produces no call. Re-running a failed batch is safe.
  • Defined partial-failure behavior — stop on first failure by default, report what was applied, leave a rollback file for those rows. --continue-on-error opts into partial application.
  • Rollback captured before the change, not reconstructed after a failure. The rollback file is itself a valid input file.
  • Voicemail modeled correctly — forward-to-voicemail is a boolean flag, matching the CUCM data model, not a fake destination string.

Quickstart (no cluster required)

git clone https://github.com/ahmadalkayyali/cfwd.git
cd cfwd
export PYTHONPATH=.          # or: pip install -e .

# 1. Look at a directory number
python -m cfwd.cli status --dn 513100

# 2. Dry-run a batch (default: nothing is written)
python -m cfwd.cli apply input/approved_forwarding_changes.csv

# 3. Apply it against the offline mock
python -m cfwd.cli apply input/approved_forwarding_changes.csv --apply

# 4. See validation block bad rows before they reach the cluster
python -m cfwd.cli apply input/site-failover-example.csv

Once installed (pip install -e .), the cfwd command is available directly:

cfwd status --dn 513100
cfwd apply input/approved_forwarding_changes.csv --apply
cfwd undo output/rollback_20260714_2117.csv --apply

Input file

A reviewed CSV is the source of truth for a change:

dn,partition,forward_type,destination,to_voicemail,forward_css,expires_at,reason,ticket
513100,PT_INTERNAL,CFA,513500,false,CSS_INTERNAL,2026-07-18T18:00,Holiday routing,CHG0051204
513103,PT_INTERNAL,CFB,,true,,,Busy to voicemail,CHG0051204
column meaning
dn / partition the line to change
forward_type CFA, CFB, CFNA, or CFUR
destination target number (leave blank for voicemail or to clear)
to_voicemail true/false — a flag, not a destination
forward_css Calling Search Space used for the forward
expires_at optional; schedules automatic reversal
reason / ticket required change metadata

Connecting to a real cluster

The mock is used automatically until you configure a host. For live AXL:

pip install -e ".[axl]"      # installs zeep + requests
cp .env.example .env         # then fill in host/user/password/wsdl
set -a; source .env; set +a  # Linux/macOS: export variables for the CLI
variable purpose
CFWD_HOST CUCM publisher FQDN
CFWD_USER / CFWD_PASSWORD dedicated AXL application account
CFWD_WSDL path to the AXL WSDL for your CUCM version
CFWD_MOCK=1 force the offline mock even when a host is set

Credentials are read from the environment or a secret store at run time. They are never written into the code or the input files. Use a dedicated application account with the minimum AXL role required (Standard AXL API Access).

How it fits together

input CSV → Validator → Planner → (dry-run diff) → Executor → verify → report
                                          │
                                   Rollback captured
                                   before apply runs
  • models.py — plain dataclasses; the rest of the code never sees SOAP.
  • axl_client.py — the only SOAP-aware module. ZeepAXLClient for live clusters, MockAXLClient for offline runs and tests.
  • validators.py — the checks that run before anything is applied.
  • planner.py — current → desired diffs; the basis of dry-run and idempotency.
  • executor.py — idempotent apply with defined partial-failure behavior.
  • rollback.py — captures prior state before the change.
  • io_files.py — input parsing and report writing.
  • cli.pystatus, apply, undo.

CSS reachability on a live cluster

The mock declares CSS→partition routes directly. On a real cluster, resolve reachability by reading the ordered partition list of the forward CSS through AXL and confirming the destination's partition appears in it — or, when translation/route patterns are involved, by validating the call path with Dialed Number Analyzer or another approved route-analysis method. validators.py isolates this behind css_can_reach() so you can swap in either strategy.

Scheduled expiry

Rows with an expires_at value are meant to be reversed automatically at that time by restoring the captured prior state. This repo models expiry as a first-class field and prints the schedule in the plan; wiring it to a scheduler (cron, APScheduler, or your job runner) is the one integration point left to the deploying team, since it depends on your environment.

Testing

pip install -e ".[dev]"   # installs pytest
pytest

The suite runs entirely against the offline mock and covers validation, loop detection, CSS reachability, idempotency, partial-failure handling, rollback capture, and the voicemail-flag model.

Note on the offline mock

MockAXLClient holds state in memory for the life of a single process, so it reseeds between separate CLI invocations. Idempotency is a property of the tool, demonstrated within one process (see tests/test_cfwd.py and the in-process demo in the article); it is not a property of the throwaway demo fixture across shell commands.

License

MIT — see LICENSE.

About

Validated call-forwarding automation for Cisco Unified CM using Python, AXL, dry-run validation, audit reporting, and rollback.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages