Saltearse al contenido

Presentación 7: AMI - Asterisk Manager Interface

Control y monitoreo en tiempo real de Asterisk mediante TCP/IP: eventos, acciones, seguridad y casos de uso profesionales

🎛️ Presentación 7: AMI - Asterisk Manager Interface

Sección titulada «🎛️ Presentación 7: AMI - Asterisk Manager Interface»

📡 Control y Monitoreo en Tiempo Real de tu PBX

Sección titulada «📡 Control y Monitoreo en Tiempo Real de tu PBX»

En la Presentación 4 vimos la arquitectura de Asterisk y mencionamos tres interfaces clave para integración externa:

AGI (Asterisk Gateway Interface)

Durante la llamada - Lógica de dialplan externa

  • Script ejecutado por el dialplan
  • Control síncrono del canal
  • Ideal para: IVRs dinámicos, validaciones, lógica de negocio

AMI (Asterisk Manager Interface)

Fuera de la llamada - Monitoreo y control remoto

  • Conexión TCP persistente
  • Eventos en tiempo real
  • Ideal para: dashboards, originar llamadas, gestión de PBX

ARI (Asterisk REST Interface)

Control total - Aplicaciones de telefonía externas

  • WebSockets + REST API
  • Control asíncrono completo
  • Ideal para: softphones web, aplicaciones complejas

AMI (Asterisk Manager Interface) es una interfaz cliente-servidor sobre TCP/IP que permite:

Monitorear el estado de Asterisk en tiempo real
Controlar la PBX remotamente (originar llamadas, colgar canales, recargar configuración)
Recibir eventos sobre cambios en el sistema (nuevos canales, hangups, registros)
Ejecutar comandos CLI desde aplicaciones externas
Gestionar colas, extensiones, voicemail, etc.


Comandos enviados desde el cliente hacia Asterisk para realizar operaciones.

Sintaxis:

Action: <NombreAccion>
ActionID: <identificador-opcional>
<Parámetro1>: <valor1>
<Parámetro2>: <valor2>

Ejemplo - Originar una llamada:

Action: Originate
Channel: PJSIP/1001
Exten: 9876543210
Context: salidas-celular
Priority: 1
CallerID: "Marketing <1001>"
ActionID: call-001

Asterisk responde a cada Action con un Response.

Sintaxis:

Response: Success|Error|Follows
ActionID: <mismo-id-del-action>
Message: <descripción>

Ejemplo - Respuesta exitosa:

Response: Success
ActionID: call-001
Message: Originate successfully queued

Mensajes asíncronos enviados por Asterisk cuando ocurre algo en el sistema.

Sintaxis:

Event: <NombreEvento>
<Campo1>: <valor1>
<Campo2>: <valor2>

Ejemplo - Nuevo canal creado:

Event: Newchannel
Privilege: call,all
Channel: PJSIP/1001-00000001
ChannelState: 0
ChannelStateDesc: Down
CallerIDNum: 1001
CallerIDName: Juan Perez
Uniqueid: 1710876543.1
Linkedid: 1710876543.1

ActionPropósitoCaso de Uso
OriginateOriginar llamadaClick-to-call, marcadores automáticos
HangupColgar canalTerminar llamadas problemáticas
PJSIPShowEndpointsListar endpoints PJSIPMonitoreo de extensiones
QueueStatusEstado de colasDashboard de call center
QueueAdd/RemoveGestionar agentesAdministración dinámica de colas
CommandEjecutar comando CLIAutomatización de tareas
ReloadRecargar módulosAplicar cambios sin reiniciar
CoreShowChannelsListar canales activosMonitoreo de llamadas en curso
GetVarObtener variable de canalDebugging y análisis
RedirectTransferir llamadaEnrutamiento dinámico

Eventos de Canal (Ciclo de Vida de Llamadas)

Sección titulada «Eventos de Canal (Ciclo de Vida de Llamadas)»
EventCuándo se DisparaUso
NewchannelSe crea un nuevo canalDetectar inicio de llamada
NewstateCambia el estado del canalSeguir progreso (Ringing, Up, etc.)
HangupCanal es colgadoDetectar fin de llamada
DialBeginInicia marcadoMonitorear intentos de llamada
DialEndTermina marcadoVer resultado (ANSWER, BUSY, etc.)
EventCuándo se DisparaUso
PeerStatusEndpoint se registra/desregistraMonitoreo de disponibilidad
ContactStatusCambia estado de contactoVer latencia y calidad
ContactStatusDetailDetalles de contactoInformación completa de registro
EventCuándo se DisparaUso
QueueMemberAddedAgente se agrega a colaTracking de agentes
QueueMemberRemovedAgente se remueveGestión de disponibilidad
QueueMemberStatusCambia estado de agenteMonitoreo en tiempo real
AgentCalledAgente recibe llamadaMétricas de distribución
AgentCompleteAgente termina llamadaCálculo de tiempos

; /etc/asterisk/manager.conf
[general]
enabled = yes ; Habilitar AMI
port = 5038 ; Puerto TCP (estándar)
bindaddr = 0.0.0.0 ; Escuchar en todas las interfaces
; === SEGURIDAD ===
; Agregar timestamp a eventos (útil para logs)
timestampevents = yes
; Tiempo máximo para autenticación (segundos)
authtimeout = 30
; Máximo de sesiones sin autenticar simultáneas
authlimit = 50
; === OPTIMIZACIÓN DE RENDIMIENTO ===
; Deshabilitar eventos globalmente que no necesites
; Esto previene que se generen, ahorrando CPU
disabledevents = Newexten,VarSet
; === USUARIOS AMI ===
; Cada aplicación debe tener su propio usuario
[panel_web]
secret = Panel2025Secure!
; Denegar todo por defecto
deny = 0.0.0.0/0.0.0.0
; Permitir solo desde localhost y red local
permit = 127.0.0.1/255.255.255.0
permit = 192.168.1.0/255.255.255.0
; Permisos de lectura (recibir eventos)
read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan
; Permisos de escritura (ejecutar acciones)
write = system,call,agent,user,config,command,reporting,originate
[monitor_only]
secret = Monitor2025!
deny = 0.0.0.0/0.0.0.0
permit = 192.168.1.100/255.255.255.255
; Solo lectura - para dashboards de monitoreo
read = system,call,agent,reporting,cdr
write = ; Sin permisos de escritura
ClaseLectura (read)Escritura (write)
systemEventos del sistemaShutdown, Restart, Reload
callEventos de canalesHangup, Redirect, Atxfer
logEventos de logging-
verboseMensajes verbose-
agentEventos de colas/agentesQueueAdd, QueueRemove
userUserEventEnviar UserEvent
config-Leer/escribir archivos config
command-Ejecutar comandos CLI
dtmfEventos DTMF-
reportingInformación del sistema-
cdrEventos CDR-
dialplanNewexten, VarSet-
originate-Originar llamadas
allTodos los eventosTodas las acciones

🎛️ Optimización de Eventos: disabledevents vs eventfilter

Sección titulada «🎛️ Optimización de Eventos: disabledevents vs eventfilter»

En un sistema con 100 extensiones y 50 llamadas simultáneas, AMI puede generar miles de eventos por minuto. Muchos de estos eventos no son relevantes para ninguna aplicación y consumen recursos innecesariamente.

🚀 Solución 1: disabledevents (Global - RECOMENDADO)

Sección titulada «🚀 Solución 1: disabledevents (Global - RECOMENDADO)»

La forma más eficiente de optimizar rendimiento. Desactiva la generación de eventos a nivel global.

Previene la generación - El evento nunca se crea
Ahorra CPU - No se ejecuta código de generación
Ahorra memoria - No se serializa ni se mantiene en buffer
Afecta a todos los usuarios - Configuración centralizada
Ideal para eventos que NADIE necesita

[general]
enabled = yes
port = 5038
bindaddr = 0.0.0.0
; === OPTIMIZACIÓN CRÍTICA DE RENDIMIENTO ===
; Deshabilitar eventos que NINGÚN cliente necesita
; Estos eventos se generan miles de veces por segundo en sistemas con tráfico
; Eventos de dialplan (muy frecuentes, raramente útiles)
; - Newexten: Se dispara en cada paso del dialplan
; - VarSet: Se dispara cada vez que se establece una variable
disabledevents = Newexten,VarSet
; Eventos RTP (CRÍTICO en sistemas con muchas llamadas)
; - RTCPSent/RTCPReceived: Se generan cada 5 segundos por cada canal activo
; - En un sistema con 50 llamadas = 600 eventos RTP por minuto
; Si NINGÚN cliente AMI necesita estadísticas RTP, deshabilitarlos aquí:
;disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived

Escenario: Call center con 100 agentes, 80 llamadas simultáneas promedio

Sin disabledevents:

Newexten: ~500 eventos/minuto
VarSet: ~800 eventos/minuto
RTCPSent: ~480 eventos/minuto
RTCPReceived: ~480 eventos/minuto
TOTAL: ~2,260 eventos/minuto generados (pero no usados)

Con disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived:

TOTAL: 0 eventos innecesarios generados
Reducción de carga CPU: ~15-25%

🎯 Solución 2: eventfilter (Por Usuario - Granular)

Sección titulada «🎯 Solución 2: eventfilter (Por Usuario - Granular)»

Usa esto cuando diferentes usuarios AMI necesitan eventos diferentes. Los eventos SÍ se generan, pero se filtran antes de enviarlos.

Granular - Cada usuario tiene filtros diferentes
Flexible - Incluir/excluir por nombre, header, regex
Ideal cuando algunos usuarios SÍ necesitan el evento
⚠️ El evento SÍ se genera - Consume CPU/memoria
⚠️ Solo filtra el envío - No previene la generación

eventfilter(<criterios>) = [expresión]

Criterios disponibles:

  • action(include|exclude) - Incluir o excluir
  • name(<nombre_evento>) - Filtrar por nombre exacto (usa hash, muy eficiente)
  • header(<nombre_header>) - Filtrar por header específico
  • method(regex|exact|starts_with|ends_with|contains|none) - Método de comparación

1. Dashboard general - Solo eventos de canales PJSIP

[dashboard]
secret = Dashboard2025!
read = system,call,agent,reporting
write = originate
; Incluir solo eventos de llamadas
eventfilter(action(include),name(Newchannel)) =
eventfilter(action(include),name(Hangup)) =
eventfilter(action(include),name(DialBegin)) =
eventfilter(action(include),name(DialEnd)) =
; Excluir canales Local/ (internos)
eventfilter(action(exclude),header(Channel),method(starts_with)) = Local/

2. Monitor de cola específica

[queue_monitor_soporte]
secret = QueueMon2025!
read = agent,call
write =
; Solo eventos de la cola "soporte"
eventfilter(action(include),name(QueueMemberStatus),header(Queue),method(exact)) = soporte
eventfilter(action(include),name(AgentCalled),header(Queue),method(exact)) = soporte
eventfilter(action(include),name(AgentComplete),header(Queue),method(exact)) = soporte

3. Monitor de extensión específica

[extension_monitor_1001]
secret = ExtMon2025!
read = call
write =
; Solo eventos del endpoint 1001
eventfilter(action(include),name(Newchannel),header(CallerIDNum),method(exact)) = 1001
eventfilter(action(include),name(Hangup),header(CallerIDNum),method(exact)) = 1001
eventfilter(action(include),name(PeerStatus),header(Peer),method(contains)) = 1001

Criteriodisabledeventseventfilter
Rendimiento⭐⭐⭐⭐⭐ Máximo⭐⭐⭐ Bueno
AlcanceGlobal (todos los usuarios)Por usuario
Generación del evento❌ NO se genera✅ SÍ se genera
Consumo CPUMínimoModerado (evalúa filtros)
Consumo memoriaMínimoModerado (serializa eventos)
FlexibilidadBaja (todo o nada)Alta (granular)
Uso idealEventos que NADIE necesitaEventos que ALGUNOS necesitan
[general]
enabled = yes
port = 5038
; === PASO 1: Deshabilitar eventos que NADIE necesita (máximo rendimiento) ===
disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived
; === PASO 2: Usuarios con filtros granulares ===
[dashboard_general]
secret = Dashboard2025!
read = system,call,agent,reporting
write = originate
; Este usuario recibe todos los eventos (excepto los deshabilitados globalmente)
[monitor_cola_ventas]
secret = QueueVentas2025!
read = agent,call
write =
; Este usuario solo recibe eventos de la cola "ventas"
eventfilter(action(include),name(QueueMemberStatus),header(Queue),method(exact)) = ventas
eventfilter(action(include),name(AgentCalled),header(Queue),method(exact)) = ventas
eventfilter(action(include),name(AgentComplete),header(Queue),method(exact)) = ventas

Terminal window
# Conectar a AMI
telnet localhost 5038
# Asterisk responde con banner:
# Asterisk Call Manager/9.2.0
# Autenticarse
Action: Login
Username: panel_web
Secret: Panel2025Secure!
# Respuesta:
# Response: Success
# Message: Authentication accepted
# Listar endpoints
Action: PJSIPShowEndpoints
# Cerrar sesión
Action: Logoff

💻 Ejemplo Real: Originar Llamada Click-to-Call

Sección titulada «💻 Ejemplo Real: Originar Llamada Click-to-Call»

Un agente de soporte ve un ticket en el CRM y hace clic en el número del cliente. El sistema debe:

  1. Llamar primero al teléfono del agente (extensión 1001)
  2. Cuando el agente contesta, llamar al cliente
  3. Conectar ambas partes
click_to_call.py
import socket
import time
def ami_connect(host, port, username, secret):
"""Conectar y autenticar en AMI"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
# Leer banner
banner = sock.recv(1024).decode()
print(f"Conectado: {banner}")
# Autenticar
login = f"Action: Login\r\nUsername: {username}\r\nSecret: {secret}\r\n\r\n"
sock.send(login.encode())
response = sock.recv(1024).decode()
if "Success" in response:
print("✅ Autenticación exitosa")
return sock
else:
raise Exception("❌ Autenticación fallida")
def originate_call(sock, agent_ext, customer_number):
"""Originar llamada click-to-call"""
action_id = f"call-{int(time.time())}"
action = f"""Action: Originate
Channel: PJSIP/{agent_ext}
Exten: {customer_number}
Context: salidas-celular
Priority: 1
CallerID: "Cliente <{customer_number}>"
Timeout: 30000
ActionID: {action_id}
"""
sock.send(action.encode())
print(f"📞 Llamada iniciada: {agent_ext}{customer_number}")
# Leer respuesta
response = sock.recv(4096).decode()
print(f"Respuesta: {response}")
# Uso
if __name__ == "__main__":
sock = ami_connect("localhost", 5038, "panel_web", "Panel2025Secure!")
# Click-to-call: agente 1001 llama a cliente
originate_call(sock, "1001", "987654321")
# Cerrar conexión
sock.send(b"Action: Logoff\r\n\r\n")
sock.close()

Monitoreo de Extensiones con SSE (Server-Sent Events)

Sección titulada «Monitoreo de Extensiones con SSE (Server-Sent Events)»

El panel web del Taller 9 implementa este patrón:

// Frontend: Consumir eventos AMI vía SSE
const eventSource = new EventSource('/api/ami/events');
eventSource.addEventListener('PeerStatus', (event) => {
const data = JSON.parse(event.data);
// Actualizar UI según estado
if (data.PeerStatus === 'Reachable') {
updateExtensionStatus(data.Peer, 'online', data.Time);
} else {
updateExtensionStatus(data.Peer, 'offline');
}
});
eventSource.addEventListener('Newchannel', (event) => {
const data = JSON.parse(event.data);
addActiveCall(data.Channel, data.CallerIDNum);
});
eventSource.addEventListener('Hangup', (event) => {
const data = JSON.parse(event.data);
removeActiveCall(data.Channel);
});
// Backend: Convertir eventos AMI a SSE
import { EventEmitter } from 'events';
import { amiManager } from '@/lib/ami-manager';
export async function GET(request: Request) {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// Escuchar eventos AMI
const handleEvent = (event: any) => {
const sseData = `event: ${event.Event}\ndata: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(sseData));
};
amiManager.on('PeerStatus', handleEvent);
amiManager.on('Newchannel', handleEvent);
amiManager.on('Hangup', handleEvent);
// Cleanup al cerrar conexión
request.signal.addEventListener('abort', () => {
amiManager.off('PeerStatus', handleEvent);
amiManager.off('Newchannel', handleEvent);
amiManager.off('Hangup', handleEvent);
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}

Firewall estricto

Terminal window
# Solo permitir AMI desde localhost
ufw deny 5038
ufw allow from 127.0.0.1 to any port 5038
# Si necesitas acceso remoto, usa VPN o SSH tunnel
ssh -L 5038:localhost:5038 user@asterisk-server

Contraseñas fuertes

Terminal window
# Generar contraseña segura
openssl rand -base64 32

Permisos granulares

; Nunca uses esto en producción:
; read = all
; write = all
; Usa permisos específicos:
read = call,agent,reporting
write = originate

Deshabilitar eventos innecesarios

[general]
; Prevenir generación de eventos que no usas
disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived

Logs de auditoría

[general]
; Registrar todas las conexiones AMI
displayconnects = yes

TLS para conexiones remotas

[general]
tlsenable = yes
tlsbindaddr = 0.0.0.0:5039
tlscertfile = /etc/asterisk/keys/asterisk.pem
tlsprivatekey = /etc/asterisk/keys/asterisk.key

Terminal window
# Verificar puerto escuchando
netstat -tlnp | grep 5038
# Verificar en CLI
asterisk -rx "manager show settings"
asterisk -rx "manager show users"
asterisk -rx "manager show connected"
Terminal window
# Ver logs en tiempo real
tail -f /var/log/asterisk/full | grep -i manager
# Habilitar debug de AMI
asterisk -rx "manager set debug on"
# Ver comandos disponibles
asterisk -rx "manager show commands"
ErrorCausaSolución
Connection refusedAMI no habilitadoenabled = yes en manager.conf
Authentication failedCredenciales incorrectasVerificar username/secret
Permission deniedPermisos insuficientesAjustar read/write en usuario
No events receivedFiltros muy restrictivosRevisar eventfilter
Connection timeoutFirewall bloqueandoVerificar ufw/iptables

  • asterisk-manager - Cliente AMI completo con EventEmitter
  • ami-io - Cliente moderno con Promises
  • panoramisk - Cliente AMI asíncrono (asyncio)
  • pyst2 - Suite completa (AGI + AMI)
  • PHPAMI - Cliente AMI orientado a objetos
  • PAMI - PHP Asterisk Manager Interface
  • go-ami - Cliente AMI nativo en Go

  • AMI es la interfaz TCP/IP para control y monitoreo remoto de Asterisk
  • Actions son comandos del cliente hacia Asterisk
  • Responses son respuestas síncronas a Actions
  • Events son notificaciones asíncronas del sistema
  • Filtros de eventos optimizan el rendimiento en sistemas de alto tráfico
  • Permisos granulares son críticos para seguridad

Ideal para:

  • Dashboards de monitoreo en tiempo real
  • Paneles de administración web
  • Click-to-call desde CRM
  • Gestión remota de colas y agentes
  • Reportes en vivo de call center

No usar para:

  • Lógica de dialplan (usa AGI)
  • Aplicaciones de telefonía complejas (usa ARI)
  • Control de medios (usa ARI + WebRTC)

🚀 Taller 9 - Panel de Administración Web con Next.js

¿Listos para construir un panel profesional? En el próximo taller, implementaremos un dashboard completo con Next.js que usa AMI para monitoreo en tiempo real, gestión de usuarios con roles, y visualización de reportes CDR/CEL.

Verás AMI en acción con:

  • Monitoreo de extensiones en tiempo real vía SSE
  • Originar y colgar llamadas desde la web
  • Reportes CDR/CEL con filtros avanzados
  • Sistema de autenticación con roles

👉 Ir al Taller 9: Panel de Administración Web