support task-selectable SIP trunks
This commit is contained in:
+49
-19
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
PUBLIC_IP = "123.56.71.98"
|
||||
CODEC_TO_ASTERISK = {"PCMA": "alaw", "PCMU": "ulaw"}
|
||||
TRUNK_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}")
|
||||
|
||||
|
||||
def scalar(value, name, secret=False):
|
||||
@@ -115,6 +116,47 @@ def endpoint(name, data, env):
|
||||
return text + auth
|
||||
|
||||
|
||||
def _trunks(cfg):
|
||||
if "trunks" not in cfg:
|
||||
primary_data = cfg.get("primary", {})
|
||||
if not isinstance(primary_data, dict):
|
||||
raise ValueError("primary must be an object")
|
||||
rows = [("provider-primary", primary_data)]
|
||||
backup_data = cfg.get("backup")
|
||||
if backup_data is not None and not isinstance(backup_data, dict):
|
||||
raise ValueError("backup must be an object when configured")
|
||||
if backup_data:
|
||||
rows.append(("provider-backup", backup_data))
|
||||
if (primary_data.get("host"), primary_data.get("port", 5060)) == (
|
||||
backup_data.get("host"),
|
||||
backup_data.get("port", 5060),
|
||||
):
|
||||
raise ValueError(
|
||||
"primary and backup targets must be distinct; shared failure domains still require validation"
|
||||
)
|
||||
return rows
|
||||
if "primary" in cfg or "backup" in cfg:
|
||||
raise ValueError("use trunks instead of mixing arbitrary trunks with primary/backup")
|
||||
raw = cfg["trunks"]
|
||||
if not isinstance(raw, list) or not raw:
|
||||
raise ValueError("trunks must be a non-empty list")
|
||||
rows = []
|
||||
seen = set()
|
||||
for index, data in enumerate(raw):
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError(f"trunks[{index}] must be an object")
|
||||
name = data.get("trunk_id")
|
||||
if not isinstance(name, str) or not TRUNK_ID_RE.fullmatch(name):
|
||||
raise ValueError(
|
||||
f"trunks[{index}].trunk_id must be a safe Asterisk endpoint identifier"
|
||||
)
|
||||
if name in seen:
|
||||
raise ValueError(f"duplicate trunk_id: {name}")
|
||||
seen.add(name)
|
||||
rows.append((name, data))
|
||||
return rows
|
||||
|
||||
|
||||
def render(cfg, env):
|
||||
if cfg.get("public_ip") != PUBLIC_IP or cfg.get("transport") != "udp":
|
||||
raise ValueError(
|
||||
@@ -138,32 +180,20 @@ def render(cfg, env):
|
||||
password = scalar(env.get("ARI_PASSWORD"), "ARI_PASSWORD", secret=True)
|
||||
if len(password) < 32:
|
||||
raise ValueError("ARI_PASSWORD must have at least 32 characters")
|
||||
primary_data = cfg.get("primary", {})
|
||||
primary = endpoint("provider-primary", primary_data, env)
|
||||
backup_data = cfg.get("backup")
|
||||
if backup_data is not None and not isinstance(backup_data, dict):
|
||||
raise ValueError("backup must be an object when configured")
|
||||
sections = [primary]
|
||||
if backup_data:
|
||||
backup = endpoint("provider-backup", backup_data, env)
|
||||
if (primary_data["host"], primary_data.get("port", 5060)) == (
|
||||
backup_data["host"],
|
||||
backup_data.get("port", 5060),
|
||||
):
|
||||
raise ValueError(
|
||||
"primary and backup targets must be distinct; shared failure domains still require validation"
|
||||
)
|
||||
sections.append(backup)
|
||||
trunks = _trunks(cfg)
|
||||
sections = [endpoint(name, data, env) for name, data in trunks]
|
||||
primary_name, primary_data = trunks[0]
|
||||
transport = f"[global]\ntype=global\nuser_agent=agent-call\n\n[transport-udp]\ntype=transport\nprotocol=udp\nbind=0.0.0.0:5060\nlocal_net={network}\nexternal_signaling_address={PUBLIC_IP}\nexternal_media_address={PUBLIC_IP}\n\n"
|
||||
caller = primary_data.get("caller_id") or primary_data.get("from_user")
|
||||
extensions = "[deny-inbound]\nexten => s,1,Hangup()\nexten => _.,1,Hangup()\n"
|
||||
if caller:
|
||||
caller = scalar(caller, "primary.caller_id")
|
||||
caller = scalar(caller, f"{primary_name}.caller_id")
|
||||
extensions += (
|
||||
"\n[outbound]\n"
|
||||
"exten => _X.,1,NoOp(agent-call outbound)\n"
|
||||
"; ARI Cell calls select an approved endpoint with PJSIP/<number>@<trunk_id>.\n"
|
||||
"exten => _X.,1,NoOp(agent-call outbound fallback)\n"
|
||||
f" same => n,Set(CALLERID(all)={caller})\n"
|
||||
" same => n,Dial(PJSIP/${EXTEN}@provider-primary,60)\n"
|
||||
f" same => n,Dial(PJSIP/${{EXTEN}}@{primary_name},60)\n"
|
||||
" same => n,Hangup()\n"
|
||||
)
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user