Taller 5: Ruleteo de CallerID Dinámico con AGI y Python
Rotación dinámica de CallerID de salida desacoplando la lógica del dialplan con AGI (Python)
Taller 5: Ruleteo de CallerID Dinámico con AGI y Python
Sección titulada «Taller 5: Ruleteo de CallerID Dinámico con AGI y Python»El Problema Real: CallerID Estático y Bajas Tasas de Respuesta
Sección titulada «El Problema Real: CallerID Estático y Bajas Tasas de Respuesta»Una empresa de marketing necesita realizar llamadas salientes, pero se ha dado cuenta de que si usa siempre el mismo número de CallerID, la tasa de respuesta de los clientes disminuye. Necesitan una forma de rotar entre una lista de números de teléfono válidos de forma aleatoria, y esta lista debe poder actualizarse fácilmente sin tener que recargar el dialplan de Asterisk.
La Solución de Producción: Externalizar la Lógica con AGI
Sección titulada «La Solución de Producción: Externalizar la Lógica con AGI»En lugar de codificar la lógica de rotación dentro del extensions.conf, lo cual sería rígido y difícil de mantener, le daremos esa responsabilidad a un script externo. El dialplan simplemente le preguntará al script: “Voy a llamar a este número, ¿qué CallerID debo usar?”. El script responderá, y el dialplan continuará. Esto es el poder de AGI (Asterisk Gateway Interface).
5.0.- Buenas Prácticas: Backup y Seguridad Primero
Sección titulada «5.0.- Buenas Prácticas: Backup y Seguridad Primero»Antes de modificar configuración o introducir scripts, aseguremos nuestro entorno.
5.0.1- Backup de configuración de Asterisk
Sección titulada «5.0.1- Backup de configuración de Asterisk»# Estando en /etc/asteriskcd /etc/asteriskcp -rfv . ../asterisk.bk-$(date +%F-%H%M)Esto creará una carpeta asterisk.bk-YYYY-MM-DD-HHMM con el estado actual. Si algo falla, restaura desde ahí.
5.0.2- Cortafuegos (UFW) y puertos requeridos
Sección titulada «5.0.2- Cortafuegos (UFW) y puertos requeridos»Si aún no lo hiciste en el Taller 4, limita la superficie de ataque permitiendo solo lo necesario para SIP/RTP y acceso administrativo.
# Señalización SIP UDPufw allow 5060/udp
# Rango RTP por defectoufw allow 10000:20000/udp
# (Opcional) Restringe por IP de tu red/operadorufw allow from 192.168.1.0/24 to any port 5060 proto udpufw allow from 192.168.1.0/24 to any port 10000:20000 proto udp5.1.- Preparación del Entorno
Sección titulada «5.1.- Preparación del Entorno»Antes de escribir nuestro script, necesitamos instalar el intérprete de Python y asegurarnos de que nuestro Asterisk esté listo.
# Instalamos Python 3 en nuestro servidor Debian.apt updateapt install python3 -y
# Verificamos que se instaló correctamente.python3 --version5.2.- Refactorizando el Dialplan para Escalar
Sección titulada «5.2.- Refactorizando el Dialplan para Escalar»Nuestro extensions.conf actual es simple, pero a medida que nuestra PBX crezca, se volverá inmanejable. Vamos a reestructurarlo ahora, separando las responsabilidades en diferentes archivos, una práctica estándar en producción.
5.2.1- Modificar extensions.conf para usar #include
Sección titulada «5.2.1- Modificar extensions.conf para usar #include»Vamos a convertir nuestro extensions.conf en el “índice” principal de nuestro dialplan.
# Estando en /etc/asterisk/nano extensions.confModifica el archivo para que se vea así. Hemos comentado la lógica anterior y la hemos reemplazado con directivas #include.
[general]static=yeswriteprotect=noclearglobalvars=no
[globals]; Por ahora, esta sección permanece vacía.
; Incluimos nuestros nuevos archivos de configuración modulares#include extensions_anexos.conf#include extensions_salidas.conf#include extensions_funciones.conf#include extensions_categorias.conf
;[internal]; Lógica para llamar entre extensiones internas (1001, 1002, 1003);exten => _100X,1,NoOp(Llamada interna a la extension ${EXTEN}); same => n,Dial(PJSIP/${EXTEN},30,tT); same => n,Hangup()
; Extensión de prueba para reproducir un sonido;exten => 500,1,NoOp(Iniciando prueba de audio); same => n,Answer(); same => n,Playback(hello-world); same => n,Hangup()5.2.2- Crear los Nuevos Archivos de Configuración
Sección titulada «5.2.2- Crear los Nuevos Archivos de Configuración»Ahora, crearemos los archivos que acabamos de referenciar.
# Estando en /etc/asterisk/touch extensions_anexos.conftouch extensions_salidas.conftouch extensions_funciones.conftouch extensions_categorias.conf5.2.3- Poblar los Nuevos Archivos
Sección titulada «5.2.3- Poblar los Nuevos Archivos»-
Mover la lógica de anexos a
extensions_anexos.conf:Terminal window nano extensions_anexos.confPega la lógica de llamadas internas que comentamos antes:
[anexos-internos]; Lógica para llamar entre extensiones internas (1001, 1002, 1003)exten => _100X,1,NoOp(Llamada interna a la extension ${EXTEN})same => n,Dial(PJSIP/${EXTEN},30,tT)same => n,Hangup() -
Crear las categorías de permisos en
extensions_categorias.conf:Terminal window nano extensions_categorias.confEsta es la base para un sistema de permisos real. Cada categoría incluye los contextos a los que tiene acceso.
[cat1] ; Categoría con todos los permisosinclude => anexos-internosinclude => salidas-celular[cat2] ; Categoría solo con permisos internosinclude => anexos-internos[cat3] ; Categoría solo con permisos de salidainclude => anexos-internos -
Modificar
pjsip.confpara usar las nuevas categorías: Debemos asignar cada endpoint a una categoría para que el sistema de permisos funcione. En lugar de fijar elcontexten la plantilla, lo definiremos por extensión para tener control fino (cat1/cat2/cat3).Terminal window nano pjsip.confEdita
pjsip.confasí: deja elcontextgeneral en la plantilla (o remuévelo) y ASIGNA elcontextexplícitamente por anexo.;================================================================================; TRANSPORTES - Cómo escucha Asterisk;================================================================================[transport-udp]type=transportprotocol=udpbind=0.0.0.0:5060;================================================================================; PLANTILLAS - Para no repetir código (¡Mejores Prácticas!);================================================================================[endpoint-template](!)type=endpoint; context=internal ; opcional aquí; preferimos setear por anexo abajodisallow=allallow=ulawallow=alawallow=opustransport=transport-udp[auth-template](!)type=authauth_type=userpass[aor-template](!)type=aormax_contacts=1qualify_frequency=60;================================================================================; EXTENSIONES - Asignando context por anexo (cat1/cat2/cat3);================================================================================; --- Extensión 1001: Usuario de Oficina (cat1: internos + salidas) ---[1001](endpoint-template)context=cat1auth=1001aors=1001callerid = Usuario Oficina <1001>[1001](auth-template)username=1001password=pass1001[1001](aor-template)max_contacts=10; --- Extensión 1002: Soporte (cat2: solo internos) ---[1002](endpoint-template)context=cat2auth=1002aors=1002callerid = Agente Soporte <1002>[1002](auth-template)username=1002password=pass1002[1002](aor-template)max_contacts=2; --- Extensión 1003: Almacén (cat3: política especial) ---[1003](endpoint-template)context=cat3auth=1003aors=1003callerid = Almacen <1003>[1003](auth-template)username=1003password=pass1003[1003](aor-template); max_contacts por defecto (1)
5.3.- El Script AGI: El Cerebro Externo
Sección titulada «5.3.- El Script AGI: El Cerebro Externo»Ahora viene la parte más emocionante. Crearemos nuestro script de Python que manejará la lógica de rotación de CallerID.
5.3.0- AGI en 5 minutos (fundamentos)
Sección titulada «5.3.0- AGI en 5 minutos (fundamentos)»¿Qué es AGI? El Asterisk Gateway Interface es un protocolo muy simple que permite que Asterisk “delegue” decisiones a un programa externo. Asterisk arranca tu script como si fuera un ejecutable local y se comunica con él por texto plano.
- Canal de comunicación: entrada/salida estándar (STDIN/STDOUT)
- Formato: líneas de texto con
clave: valor(variables) y comandos AGI comoSET VARIABLE,STREAM FILE, etc. - Modelo: Asterisk invoca el script cuando alcanza la aplicación
AGI()en el dialplan, el script ejecuta lógica, responde, y termina; el flujo vuelve al dialplan.
Ciclo de vida de una ejecución AGI:
- Asterisk invoca tu script (por ejemplo
AGI(callerid_roulette.py)). - Envía variables de canal por STDIN (una por línea) y una línea en blanco.
- Tu script lee esas líneas, toma decisiones y escribe comandos AGI por STDOUT.
- Asterisk ejecuta cada comando y responde con
200 result=...por STDIN. - Tu script finaliza (
exit 0) y el dialplan continúa en la siguiente prioridad.
Comandos AGI comunes:
SET VARIABLE <name> <value>GET VARIABLE <name>STREAM FILE <soundfile> "digits"SAY NUMBER <num>EXEC <app> <args>(ejecuta aplicaciones del dialplan)
FastAGI (AGI por red):
Para alto rendimiento, en lugar de lanzar procesos en cada llamada, puedes usar FastAGI (AGI sobre TCP). AGI(agi://host:port/endpoint) conecta con tu servidor FastAGI (por ejemplo, un microservicio) y reduce la carga en el servidor Asterisk.
- Ventajas: pooling, despliegue independiente, escalado horizontal.
- Consideraciones: firewall, latencia de red, timeouts.
Lenguajes y librerías populares:
- Python:
asterisk.agi(paquetepython-asterisk),starpy(Twisted, FastAGI) - Node.js:
agi,fastagi(npm),asterisk-amipara integración con AMI - PHP:
phpagi(AGI y FastAGI) - Go:
go-agi,go-asterisk/agi - Ruby:
ruby-agi
Ejemplo mínimo (pseudocódigo):
leer lineas hasta linea en blanco -> envescribir "SET VARIABLE FOO bar" + "\n" -> flushleer respuesta ("200 result=1")salir 0Ejemplo mínimo en Python (muy simple):
#!/usr/bin/env python3import sys
# 1) Leer variables de entorno del canalwhile True: line = sys.stdin.readline().strip() if line == "": break # línea en blanco: fin de cabecera AGI
# 2) Enviar un comando AGI (setear variable)sys.stdout.write('SET VARIABLE FOO "bar"\n')sys.stdout.flush()
# 3) Leer la respuesta del comando_resp = sys.stdin.readline()
# 4) Terminarsys.exit(0)Debug y herramientas:
- CLI:
agi set debug onpara ver intercambio AGI en tiempo real. - Logs: usa
VERBOSEdesde el script para imprimir en CLI. - Tiempos: evita operaciones lentas (esperas largas) para no bloquear el canal.
5.3.1- Entendiendo el Protocolo AGI
Sección titulada «5.3.1- Entendiendo el Protocolo AGI»AGI es un protocolo sorprendentemente simple. Asterisk lo ejecuta y se comunica con él a través de la entrada y salida estándar (STDIN/STDOUT):
- Asterisk envía variables de canal al script a través de STDIN.
- El script lee estas variables para entender el estado de la llamada.
- El script envía comandos AGI a Asterisk a través de STDOUT (usando
print()en Python). - Asterisk ejecuta estos comandos y devuelve el resultado al script por STDIN.
- El script finaliza, devolviendo el control al dialplan.
5.3.2- Creando el Script callerid_roulette.py
Sección titulada «5.3.2- Creando el Script callerid_roulette.py»nano /var/lib/asterisk/agi-bin/callerid_roulette.pyPega el siguiente código de Python en el archivo:
#!/usr/bin/env python3import sysimport random
# --- La Lógica de Negocio ---# Esta es la lista de CallerIDs que queremos rotar.# ¡Podemos cambiar esta lista en cualquier momento sin tocar Asterisk!CALLERID_POOL = [ "912345671", "912345672", "912345673", "912345674",]# -----------------------------
def agi_log(message): """ Función para enviar mensajes de log al CLI de Asterisk. """ sys.stdout.write(f'VERBOSE "{message}" 1\n') sys.stdout.flush()
def main(): """ Función principal del script AGI. """ # El protocolo AGI es basado en texto, línea por línea. # Leemos la primera línea para "despertar" el canal. # No es estrictamente necesario leer todas las variables, # pero es una buena práctica para limpiar el buffer. env = {} while True: line = sys.stdin.readline().strip() if line == '': break key, value = line.split(':', 1) env[key.strip()] = value.strip()
agi_log("--- AGI CallerID Roulette Iniciado ---")
# Seleccionamos un CallerID aleatorio de nuestra lista. chosen_callerid = random.choice(CALLERID_POOL) agi_log(f"CallerID seleccionado aleatoriamente: {chosen_callerid}")
# Este es el comando clave: le decimos a Asterisk que cambie el CallerID del canal. command = f'SET CALLERID "{chosen_callerid}"\n' sys.stdout.write(command) sys.stdout.flush()
# Leemos la respuesta de Asterisk para confirmar que el comando se ejecutó. result = sys.stdin.readline().strip() agi_log(f"Respuesta de Asterisk al comando SET CALLERID: {result}")
agi_log("--- AGI CallerID Roulette Finalizado ---")
if __name__ == "__main__": main()5.3.3- Permisos y Propiedad
Sección titulada «5.3.3- Permisos y Propiedad»El script debe ser ejecutable y pertenecer al usuario de Asterisk.
chmod +x /var/lib/asterisk/agi-bin/callerid_roulette.py5.4.- Integración Final: Llamando al AGI desde el Dialplan
Sección titulada «5.4.- Integración Final: Llamando al AGI desde el Dialplan»Ahora, conectamos todo. Crearemos un contexto de salida que use nuestro script.
# Estando en /etc/asterisk/nano extensions_salidas.confAñade el siguiente contexto:
[salidas-celular]; Este patrón coincide con llamadas a celulares en Perú (9 dígitos empezando con 9).exten => _9XXXXXXXX,1,NoOp(Llamada saliente a celular: ${EXTEN}) same => n,NoOp(CallerID original: ${CALLERID(num)})
; ¡Aquí ocurre la magia! Llamamos a nuestro script AGI. same => n,AGI(callerid_roulette.py)
same => n,NoOp(CallerID modificado por AGI: ${CALLERID(num)})
; Asumimos que tienes una troncal PJSIP llamada [mi-troncal] configurada. ; Reemplaza esto con el nombre de tu troncal real. ; same => n,Dial(PJSIP/${EXTEN}@mi-troncal,30,T)
; Para propósitos de prueba, llamaremos a una extensión interna. same => n,Dial(PJSIP/1003,30,T) ; Llamamos al teléfono del almacén para ver el CallerID.
same => n,Hangup()5.5.- ¡A Probar!
Sección titulada «5.5.- ¡A Probar!»-
Recarga el dialplan: Desde la consola de Linux:
Terminal window asterisk -rx "dialplan reload"Y recarga pjsip por los cambios de contexto:
Terminal window asterisk -rx "pjsip reload" -
Abre el CLI de Asterisk para observar:
Terminal window asterisk -rvvv -
Realiza la llamada: Desde la extensión
1001(que está encat1y tiene acceso asalidas-celular), marca un número de celular, por ejemplo987654321. -
Observa la Magia en el CLI: Verás algo como esto:
-- Executing [987654321@cat1:1] NoOp("PJSIP/1001-0000000a", "Llamada saliente a celular: 987654321") in new stack-- Executing [987654321@cat1:2] NoOp("PJSIP/1001-0000000a", "CallerID original: 1001") in new stack-- Executing [987654321@cat1:3] AGI("PJSIP/1001-0000000a", "callerid_roulette.py") in new stack-- AGI Script callerid_roulette.py completed, returning 0-- <PJSIP/1001-0000000a>AGI Script Executing Application: (VERBOSE) Options: ("--- AGI CallerID Roulette Iniciado ---" 1)-- <PJSIP/1001-0000000a>AGI Script Executing Application: (VERBOSE) Options: ("CallerID seleccionado aleatoriamente: 912345673" 1)-- <PJSIP/1001-0000000a>AGI Script Executing Application: (VERBOSE) Options: ("Respuesta de Asterisk al comando SET CALLERID: 200 result=1" 1)-- <PJSIP/1001-0000000a>AGI Script Executing Application: (VERBOSE) Options: ("--- AGI CallerID Roulette Finalizado ---" 1)-- Executing [987654321@cat1:4] NoOp("PJSIP/1001-0000000a", "CallerID modificado por AGI: 912345673") in new stack-- Executing [987654321@cat1:5] Dial("PJSIP/1001-0000000a", "PJSIP/1003,30,T") in new stack-- Called PJSIP/1003> Calling PJSIP/1003 with callerid 912345673
El Momento “Aha!”
Sección titulada «El Momento “Aha!”»Ahora, haz lo siguiente:
- NO toques Asterisk.
- Edita el script de Python:
nano /var/lib/asterisk/agi-bin/callerid_roulette.py - Cambia la lista
CALLERID_POOLa otros números. Guarda el archivo. - Vuelve a llamar.
Verás que el nuevo pool de CallerIDs se usa inmediatamente, sin necesidad de un dialplan reload. Acabas de desacoplar exitosamente la lógica de negocio de la infraestructura de telefonía.
5.6.- Alternativa sin AGI: Solo Dialplan con Gosub + RAND
Sección titulada «5.6.- Alternativa sin AGI: Solo Dialplan con Gosub + RAND»AGI es ideal para lógicas complejas, acceso a BD/APIs o despliegues escalables. Pero algunos casos pueden resolverse únicamente con dialplan limpio. Por ejemplo, ruletear un CallerID aleatorio a partir de una lista y un prefijo de país.
5.6.1 Definir una subrutina de CallerID por país (ej. Perú 51)
Sección titulada «5.6.1 Definir una subrutina de CallerID por país (ej. Perú 51)»; Subrutina que setea CALLERID(num) con números aleatorios; ARG1 = código de país (ej: 51)[pickCallerIDnum]exten => cell1,1,Set(CALLERID(num)=${ARG1}90548${RAND(1000,9999)}) same => n,Returnexten => cell2,1,Set(CALLERID(num)=${ARG1}90549${RAND(1000,9999)}) same => n,Returnexten => cell3,1,Set(CALLERID(num)=${ARG1}90550${RAND(1000,9999)}) same => n,Returnexten => cell4,1,Set(CALLERID(num)=${ARG1}911${RAND(100000,999999)}) same => n,Returnexten => cell5,1,Set(CALLERID(num)=${ARG1}909${RAND(100000,999999)}) same => n,Return5.6.2 Usarlo desde el contexto de salidas
Sección titulada «5.6.2 Usarlo desde el contexto de salidas»[salidas-celular]exten => _9XXXXXXXX,1,NoOp(Llamada saliente a celular: ${EXTEN}) same => n,Set(CP=51) ; Código de país Perú same => n,Set(R=${RAND(1,5)}) ; Escoger 1..5 same => n,NoOp(Seleccion: cell${R}) same => n,Gosub(pickCallerIDnum,cell${R},1(${CP})) same => n,NoOp(CID resultante: ${CALLERID(num)}) same => n,Dial(PJSIP/${EXTEN}@mi-proveedor-sip,45,T) same => n,Hangup()