Saltearse al contenido

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.

Terminal window
# Estando en /etc/asterisk
cd /etc/asterisk
cp -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.

Terminal window
# Señalización SIP UDP
ufw allow 5060/udp
# Rango RTP por defecto
ufw allow 10000:20000/udp
# (Opcional) Restringe por IP de tu red/operador
ufw allow from 192.168.1.0/24 to any port 5060 proto udp
ufw allow from 192.168.1.0/24 to any port 10000:20000 proto udp

Antes de escribir nuestro script, necesitamos instalar el intérprete de Python y asegurarnos de que nuestro Asterisk esté listo.

Terminal window
# Instalamos Python 3 en nuestro servidor Debian.
apt update
apt install python3 -y
# Verificamos que se instaló correctamente.
python3 --version

5.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.

Terminal window
# Estando en /etc/asterisk/
nano extensions.conf

Modifica el archivo para que se vea así. Hemos comentado la lógica anterior y la hemos reemplazado con directivas #include.

[general]
static=yes
writeprotect=no
clearglobalvars=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.

Terminal window
# Estando en /etc/asterisk/
touch extensions_anexos.conf
touch extensions_salidas.conf
touch extensions_funciones.conf
touch extensions_categorias.conf
  1. Mover la lógica de anexos a extensions_anexos.conf:

    Terminal window
    nano extensions_anexos.conf

    Pega 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()
  2. Crear las categorías de permisos en extensions_categorias.conf:

    Terminal window
    nano extensions_categorias.conf

    Esta 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 permisos
    include => anexos-internos
    include => salidas-celular
    [cat2] ; Categoría solo con permisos internos
    include => anexos-internos
    [cat3] ; Categoría solo con permisos de salida
    include => anexos-internos
  3. Modificar pjsip.conf para usar las nuevas categorías: Debemos asignar cada endpoint a una categoría para que el sistema de permisos funcione. En lugar de fijar el context en la plantilla, lo definiremos por extensión para tener control fino (cat1/cat2/cat3).

    Terminal window
    nano pjsip.conf

    Edita pjsip.conf así: deja el context general en la plantilla (o remuévelo) y ASIGNA el context explícitamente por anexo.

    ;================================================================================
    ; TRANSPORTES - Cómo escucha Asterisk
    ;================================================================================
    [transport-udp]
    type=transport
    protocol=udp
    bind=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 abajo
    disallow=all
    allow=ulaw
    allow=alaw
    allow=opus
    transport=transport-udp
    [auth-template](!)
    type=auth
    auth_type=userpass
    [aor-template](!)
    type=aor
    max_contacts=1
    qualify_frequency=60
    ;================================================================================
    ; EXTENSIONES - Asignando context por anexo (cat1/cat2/cat3)
    ;================================================================================
    ; --- Extensión 1001: Usuario de Oficina (cat1: internos + salidas) ---
    [1001](endpoint-template)
    context=cat1
    auth=1001
    aors=1001
    callerid = Usuario Oficina <1001>
    [1001](auth-template)
    username=1001
    password=pass1001
    [1001](aor-template)
    max_contacts=10
    ; --- Extensión 1002: Soporte (cat2: solo internos) ---
    [1002](endpoint-template)
    context=cat2
    auth=1002
    aors=1002
    callerid = Agente Soporte <1002>
    [1002](auth-template)
    username=1002
    password=pass1002
    [1002](aor-template)
    max_contacts=2
    ; --- Extensión 1003: Almacén (cat3: política especial) ---
    [1003](endpoint-template)
    context=cat3
    auth=1003
    aors=1003
    callerid = Almacen <1003>
    [1003](auth-template)
    username=1003
    password=pass1003
    [1003](aor-template)
    ; max_contacts por defecto (1)

Ahora viene la parte más emocionante. Crearemos nuestro script de Python que manejará la lógica de rotación de CallerID.

¿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 como SET 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:

  1. Asterisk invoca tu script (por ejemplo AGI(callerid_roulette.py)).
  2. Envía variables de canal por STDIN (una por línea) y una línea en blanco.
  3. Tu script lee esas líneas, toma decisiones y escribe comandos AGI por STDOUT.
  4. Asterisk ejecuta cada comando y responde con 200 result=... por STDIN.
  5. 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 (paquete python-asterisk), starpy (Twisted, FastAGI)
  • Node.js: agi, fastagi (npm), asterisk-ami para 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 -> env
escribir "SET VARIABLE FOO bar" + "\n" -> flush
leer respuesta ("200 result=1")
salir 0

Ejemplo mínimo en Python (muy simple):

#!/usr/bin/env python3
import sys
# 1) Leer variables de entorno del canal
while 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) Terminar
sys.exit(0)

Debug y herramientas:

  • CLI: agi set debug on para ver intercambio AGI en tiempo real.
  • Logs: usa VERBOSE desde el script para imprimir en CLI.
  • Tiempos: evita operaciones lentas (esperas largas) para no bloquear el canal.

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):

  1. Asterisk envía variables de canal al script a través de STDIN.
  2. El script lee estas variables para entender el estado de la llamada.
  3. El script envía comandos AGI a Asterisk a través de STDOUT (usando print() en Python).
  4. Asterisk ejecuta estos comandos y devuelve el resultado al script por STDIN.
  5. 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»
Terminal window
nano /var/lib/asterisk/agi-bin/callerid_roulette.py

Pega el siguiente código de Python en el archivo:

#!/usr/bin/env python3
import sys
import 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()

El script debe ser ejecutable y pertenecer al usuario de Asterisk.

Terminal window
chmod +x /var/lib/asterisk/agi-bin/callerid_roulette.py

5.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.

Terminal window
# Estando en /etc/asterisk/
nano extensions_salidas.conf

Añ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()

  1. 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"
  2. Abre el CLI de Asterisk para observar:

    Terminal window
    asterisk -rvvv
  3. Realiza la llamada: Desde la extensión 1001 (que está en cat1 y tiene acceso a salidas-celular), marca un número de celular, por ejemplo 987654321.

  4. 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

Ahora, haz lo siguiente:

  1. NO toques Asterisk.
  2. Edita el script de Python: nano /var/lib/asterisk/agi-bin/callerid_roulette.py
  3. Cambia la lista CALLERID_POOL a otros números. Guarda el archivo.
  4. 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.



¡Felicidades! Has dado tu primer gran paso en la programación avanzada de Asterisk.


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,Return
exten => cell2,1,Set(CALLERID(num)=${ARG1}90549${RAND(1000,9999)})
same => n,Return
exten => cell3,1,Set(CALLERID(num)=${ARG1}90550${RAND(1000,9999)})
same => n,Return
exten => cell4,1,Set(CALLERID(num)=${ARG1}911${RAND(100000,999999)})
same => n,Return
exten => cell5,1,Set(CALLERID(num)=${ARG1}909${RAND(100000,999999)})
same => n,Return
[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()