feat: deliver versioned SIP config to Cells
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
"transport": "udp",
|
||||
"local_net": "",
|
||||
"ari_bind": "127.0.0.1",
|
||||
"primary": {"host": "", "port": 5060, "auth_mode": "ip", "username": "", "register": false},
|
||||
"backup": {"host": "", "port": 5060, "auth_mode": "ip", "username": "", "register": false}
|
||||
"primary": {"host": "", "port": 5060, "auth_mode": "ip", "username": "", "register": false, "codec_profile": {"allowed": ["PCMA"], "preferred": "PCMA"}},
|
||||
"backup": {"host": "", "port": 5060, "auth_mode": "ip", "username": "", "register": false, "codec_profile": {"allowed": ["PCMA"], "preferred": "PCMA"}}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ if [[ "$ready" != 1 ]]; then
|
||||
echo 'Asterisk did not become CLI-ready; inspect docker logs' >&2
|
||||
exit 1
|
||||
fi
|
||||
docker exec agent-call-asterisk asterisk -rx 'pjsip set logger on'
|
||||
docker exec agent-call-asterisk asterisk -rx 'pjsip show endpoint provider-primary'
|
||||
install -d -m 0750 /var/lib/agent-call
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ >/var/lib/agent-call/bootstrap.done
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Real Cell Agent settings. Certificate files are injected by the host PKI/secret manager.
|
||||
CELL_ID=cell-beijing-01
|
||||
CELL_AGENT_HOST=10.0.0.21
|
||||
CELL_AGENT_PORT=9443
|
||||
CELL_AGENT_DB=/var/lib/agent-call/cell-agent.sqlite3
|
||||
CELL_ASTERISK_CONFIG_DIR=/etc/asterisk/pjsip.d
|
||||
CELL_ASTERISK_CLI=/usr/sbin/asterisk
|
||||
CELL_TLS_CA_FILE=/etc/agent-call/cell-tls/ca.pem
|
||||
CELL_TLS_CERT_FILE=/etc/agent-call/cell-tls/server.pem
|
||||
CELL_TLS_KEY_FILE=/etc/agent-call/cell-tls/server-key.pem
|
||||
@@ -2,8 +2,13 @@
|
||||
RABBITMQ_DEFAULT_USER=agent_call_mock
|
||||
RABBITMQ_DEFAULT_PASS=replace-before-start
|
||||
RABBITMQ_URL=amqp://agent_call_mock:replace-before-start@rabbitmq:5672/%2f
|
||||
ASR_WEB_TOKEN=local-asr-test
|
||||
# JSON map: token -> tenant IDs and scopes. Inject through a secret manager in real deployments.
|
||||
HTTP_TOKENS={"local":{"tenant_ids":["tenant-demo"],"scopes":["outbound.read","outbound.control","recording.upload","recording.complete"]}}
|
||||
# Independent SIP management credentials; synthetic local values only.
|
||||
SIP_MANAGEMENT_PORT=18090
|
||||
SIP_ADMIN_TOKENS={"admin-local":{"subject":"ops","issuer":"local","audience":"agent-call.sip-admin","scopes":["*"],"trunk_ids":"*"}}
|
||||
SIP_READ_TOKENS={"saas-local":{"subject":"saas","issuer":"local","audience":"agent-call.sip-read","scopes":["sip.trunk.read"],"trunk_ids":["trunk-mock"]}}
|
||||
AGENT_CALL_MODE=mock
|
||||
BROKER_MODE=rabbit
|
||||
AGENT_CALL_PORT=18080
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render an explicit, outbound-only UDP/PCMA baseline. Does not start services."""
|
||||
"""Render an explicit, outbound-only UDP config from the Trunk codec profile."""
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
@@ -12,6 +12,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
PUBLIC_IP = "123.56.71.98"
|
||||
CODEC_TO_ASTERISK = {"PCMA": "alaw", "PCMU": "ulaw"}
|
||||
|
||||
|
||||
def scalar(value, name, secret=False):
|
||||
@@ -29,6 +30,32 @@ def scalar(value, name, secret=False):
|
||||
return value
|
||||
|
||||
|
||||
def codec_allow(name, data):
|
||||
profile = data.get("codec_profile")
|
||||
if not isinstance(profile, dict):
|
||||
raise ValueError( # noqa: TRY004 - render() exposes one config-error type
|
||||
f"{name}.codec_profile must be explicit"
|
||||
)
|
||||
allowed = profile.get("allowed")
|
||||
if not isinstance(allowed, list) or not allowed:
|
||||
raise ValueError(f"{name}.codec_profile.allowed must be a non-empty list")
|
||||
normalized = []
|
||||
for codec in allowed:
|
||||
if not isinstance(codec, str) or codec.upper() not in CODEC_TO_ASTERISK:
|
||||
raise ValueError(f"{name}.codec_profile contains an unsupported codec")
|
||||
codec = codec.upper()
|
||||
if codec in normalized:
|
||||
raise ValueError(f"{name}.codec_profile contains a duplicate codec")
|
||||
normalized.append(codec)
|
||||
preferred = profile.get("preferred", normalized[0])
|
||||
if not isinstance(preferred, str) or preferred.upper() not in normalized:
|
||||
raise ValueError(f"{name}.codec_profile.preferred must be allowed")
|
||||
ordered = [preferred.upper()] + [
|
||||
codec for codec in normalized if codec != preferred.upper()
|
||||
]
|
||||
return ",".join(CODEC_TO_ASTERISK[codec] for codec in ordered)
|
||||
|
||||
|
||||
def endpoint(name, data, env):
|
||||
host = scalar(data.get("host"), name + ".host")
|
||||
if (
|
||||
@@ -48,7 +75,8 @@ def endpoint(name, data, env):
|
||||
registration = data.get("register", False)
|
||||
if not isinstance(registration, bool) or registration and mode != "digest":
|
||||
raise ValueError("registration requires explicit digest authentication")
|
||||
text = f"[{name}]\ntype=endpoint\ntransport=transport-udp\ncontext=deny-inbound\ndisallow=all\nallow=alaw\ndirect_media=no\nrtp_symmetric=yes\nforce_rport=yes\nrewrite_contact=yes\naors={name}-aor\n"
|
||||
allow = codec_allow(name, data)
|
||||
text = f"[{name}]\ntype=endpoint\ntransport=transport-udp\ncontext=deny-inbound\ndisallow=all\nallow={allow}\ndirect_media=no\nrtp_symmetric=yes\nforce_rport=yes\nrewrite_contact=yes\naors={name}-aor\n"
|
||||
from_user = data.get("from_user")
|
||||
if from_user is not None:
|
||||
if not isinstance(from_user, str) or not re.fullmatch(
|
||||
@@ -69,12 +97,16 @@ def endpoint(name, data, env):
|
||||
user = scalar(data.get("username"), name + ".username")
|
||||
if not re.fullmatch(r"[A-Za-z0-9_.+-]+", user):
|
||||
raise ValueError("SIP username must be a plain user identifier")
|
||||
password_key = "SIP_" + name.removeprefix("provider-").upper() + "_PASSWORD"
|
||||
password = scalar(env.get(password_key), password_key, secret=True)
|
||||
credential_env_name = (
|
||||
"SIP_" + name.removeprefix("provider-").upper() + "_PASSWORD"
|
||||
)
|
||||
credential_value = scalar(
|
||||
env.get(credential_env_name), credential_env_name, secret=True
|
||||
)
|
||||
text += f"outbound_auth={name}-auth\n"
|
||||
if from_user is None:
|
||||
text += f"from_user={user}\n"
|
||||
auth = f"\n[{name}-auth]\ntype=auth\nauth_type=userpass\nusername={user}\npassword={password}\n"
|
||||
auth = f"\n[{name}-auth]\ntype=auth\nauth_type=userpass\nusername={user}\npassword={credential_value}\n"
|
||||
if registration:
|
||||
auth += f"\n[{name}-registration]\ntype=registration\ntransport=transport-udp\noutbound_auth={name}-auth\nserver_uri=sip:{host}:{port}\nclient_uri=sip:{user}@{host}:{port}\nretry_interval=60\n"
|
||||
text += (
|
||||
|
||||
Reference in New Issue
Block a user