chore: initial project snapshot

This commit is contained in:
2026-09-11 17:47:03 +08:00
commit 23d1b1274f
45 changed files with 4468 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""CLI deployment helpers; importing this package never changes cloud resources."""
+15
View File
@@ -0,0 +1,15 @@
{
"region": "cn-beijing",
"public_ip": "123.56.71.98",
"project_tag": "ai-call",
"profile": null,
"adopt_instance_id": null,
"image_id": "",
"instance_type": "",
"vswitch_id": "",
"security_group_id": "",
"key_pair_name": "",
"spot_price_limit": null,
"system_disk_category": "cloud_essd",
"system_disk_gib": 40
}
+410
View File
@@ -0,0 +1,410 @@
#!/usr/bin/env python3
"""Read-only by default. Reuse/prepare the fixed Beijing host via the aliyun CLI."""
import argparse
import fcntl
import hashlib
import json
import math
import os
import shutil
import subprocess
import sys
import time
import uuid
from pathlib import Path
REGION = "cn-beijing"
PUBLIC_IP = "123.56.71.98"
class CloudError(RuntimeError):
pass
class AliyunCLI:
def __init__(self, profile=None):
self.binary = shutil.which("aliyun")
if not self.binary:
raise CloudError(
"aliyun CLI is not installed; configure CLI credentials locally, never paste secrets into chat"
)
self.profile = profile
def __call__(self, product, action, **params):
cmd = [self.binary, product, action, "--RegionId", REGION]
if self.profile:
cmd += ["--profile", self.profile]
for key, value in params.items():
cmd += [
"--" + key,
json.dumps(value, separators=(",", ":"))
if isinstance(value, (list, dict, bool))
else str(value),
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60, check=True
)
data = json.loads(result.stdout)
if not isinstance(data, dict):
raise CloudError(
f"{product} {action} returned an invalid response shape"
)
return data
except (
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
json.JSONDecodeError,
) as exc:
# CLI diagnostics can contain request details; don't log raw stdout/stderr or credentials.
raise CloudError(
f"{product} {action} failed; inspect the authenticated CLI locally (no mutation retry here)"
) from exc
def identity(cfg):
if cfg.get("region") != REGION or cfg.get("public_ip") != PUBLIC_IP:
raise CloudError("region/IP must remain cn-beijing / 123.56.71.98")
if (
not isinstance(cfg.get("project_tag"), str)
or not cfg["project_tag"]
or len(cfg["project_tag"]) > 64
):
raise CloudError("a dedicated project_tag is required")
def parse_count(value):
try:
count = int(value)
except (TypeError, ValueError) as exc:
raise CloudError("invalid inventory count; no mutations allowed") from exc
if isinstance(value, bool) or count < 0:
raise CloudError("invalid inventory count; no mutations allowed")
return count
def instances(api, **filters):
found = []
seen = set()
for page in range(1, 21):
data = api("ecs", "DescribeInstances", PageSize=100, PageNumber=page, **filters)
rows = data.get("Instances", {}).get("Instance")
if not isinstance(rows, list):
raise CloudError("malformed instance inventory; no mutations allowed")
for row in rows:
key = row.get("InstanceId") if isinstance(row, dict) else None
if not isinstance(key, str) or not key or key in seen:
raise CloudError(
"invalid or changing instance inventory; no mutations allowed"
)
seen.add(key)
found.extend(rows)
total = data.get("TotalCount")
if total is not None:
expected = parse_count(total)
if len(found) == expected:
return found
if len(found) > expected or not rows:
raise CloudError(
"incomplete or changing instance inventory; no mutations allowed"
)
elif len(rows) < 100:
return found
raise CloudError(
"instance inventory exceeded the safety bound; no mutations allowed"
)
def eip(api):
data = api(
"vpc", "DescribeEipAddresses", EipAddress=PUBLIC_IP, PageSize=100, PageNumber=1
)
rows = data.get("EipAddresses", {}).get("EipAddress")
if not isinstance(rows, list):
raise CloudError("malformed EIP inventory")
if parse_count(data.get("TotalCount", len(rows))) != len(rows):
raise CloudError("incomplete EIP inventory")
matches = [row for row in rows if row.get("IpAddress") == PUBLIC_IP]
if len(matches) > 1:
raise CloudError("ambiguous EIP ownership")
if matches and not matches[0].get("AllocationId"):
raise CloudError("EIP allocation ID missing; no creation allowed")
return matches[0] if matches else None
def owned(row, cfg):
tags = row.get("Tags", {}).get("Tag", [])
return row.get("InstanceId") == cfg.get("adopt_instance_id") or any(
t.get("TagKey") == "project" and t.get("TagValue") == cfg["project_tag"]
for t in tags
)
def plan(cfg, api):
identity(cfg)
address = eip(api)
if address and address.get("InstanceId"):
if address.get("InstanceType") != "EcsInstance":
raise CloudError(
"fixed EIP is attached to a non-ECS resource; refusing to rebind"
)
rows = instances(api, InstanceIds=[address["InstanceId"]])
elif address:
if address.get("Status") != "Available":
raise CloudError(
"EIP is not available; wait/reconcile instead of creating a host"
)
rows = instances(
api, **{"Tag.1.Key": "project", "Tag.1.Value": cfg["project_tag"]}
)
else:
rows = instances(api, PublicIpAddresses=[PUBLIC_IP])
rows = [
r
for r in rows
if PUBLIC_IP in r.get("PublicIpAddress", {}).get("IpAddress", [])
]
if not rows:
raise CloudError(
"fixed IP is neither an owned EIP nor an existing ECS public IP; cannot recreate this address"
)
if len(rows) > 1:
raise CloudError(
"multiple candidate instances; explicit reconciliation required"
)
if address and address.get("InstanceId") and not rows:
raise CloudError(
"EIP attachment has no visible instance; do not create or detach"
)
row = rows[0] if rows else None
if row and not owned(row, cfg):
raise CloudError(
"existing instance is not project-owned; set adopt_instance_id only after owner approval"
)
return {
"region": REGION,
"public_ip": PUBLIC_IP,
"action": "reuse"
if row and (not address or address.get("InstanceId"))
else ("bind" if row else "create_and_bind"),
"instance_id": row.get("InstanceId") if row else None,
"instance_status": row.get("Status") if row else None,
"instance_charge_type": row.get("InstanceChargeType") if row else None,
"spot_strategy": row.get("SpotStrategy") if row else None,
"allocation_id": address.get("AllocationId") if address else None,
"ip_kind": "eip" if address else "instance_public_ip",
"warning": "spot interruption can terminate calls; fixed instance public IP is not recoverable like an EIP",
}
def create_params(cfg):
required = (
"image_id",
"instance_type",
"vswitch_id",
"security_group_id",
"key_pair_name",
)
if any(
not isinstance(cfg.get(k), str)
or not cfg[k].strip()
or cfg[k].startswith(("CHANGE_ME", "--"))
for k in required
):
raise CloudError(
"creation requires approved image/type/VSwitch/security group/SSH KeyPair"
)
price = cfg.get("spot_price_limit")
if (
isinstance(price, bool)
or not isinstance(price, (int, float))
or not math.isfinite(price)
or price <= 0
):
raise CloudError(
"a positive, finite spot_price_limit approved by the owner is required"
)
disk = cfg.get("system_disk_gib", 40)
if isinstance(disk, bool) or not isinstance(disk, int) or not 40 <= disk <= 200:
raise CloudError(
"system_disk_gib must be within the approved 40200 GiB safety bound"
)
return {
"ImageId": cfg["image_id"],
"InstanceType": cfg["instance_type"],
"VSwitchId": cfg["vswitch_id"],
"SecurityGroupId": cfg["security_group_id"],
"KeyPairName": cfg["key_pair_name"],
"Amount": 1,
"InstanceName": cfg["project_tag"],
"InstanceChargeType": "PostPaid",
"InternetMaxBandwidthOut": 0,
"SpotStrategy": "SpotWithPriceLimit",
"SpotPriceLimit": price,
"SystemDisk.Category": cfg.get("system_disk_category", "cloud_essd"),
"SystemDisk.Size": disk,
"Tag.1.Key": "project",
"Tag.1.Value": cfg["project_tag"],
}
def save_state(path, data):
temporary = path.with_suffix(".tmp")
fd = os.open(
temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600
)
with os.fdopen(fd, "w") as file:
json.dump(data, file)
file.flush()
os.fsync(file.fileno())
temporary.replace(path)
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
def wait_running(api, instance_id, sleep=time.sleep):
for _ in range(60):
rows = instances(api, InstanceIds=[instance_id])
if len(rows) == 1 and rows[0].get("Status") == "Running":
return
if rows and rows[0].get("Status") in ("Stopped", "Stopping"):
raise CloudError(
f"instance {instance_id} is stopped; refusing to start or replace it automatically"
)
sleep(5)
raise CloudError(
f"instance {instance_id} is not Running; preserve state and retry/reconcile, do not create manually"
)
def apply(cfg, api, state_path, sleep=time.sleep):
state_path = Path(state_path)
state_path.parent.mkdir(parents=True, exist_ok=True)
if state_path.is_symlink():
raise CloudError("state file must not be a symlink")
lock_fd = os.open(
str(state_path) + ".lock", os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600
)
with os.fdopen(lock_fd, "w") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise CloudError(
"another provisioning process owns this state file"
) from exc
current = plan(cfg, api) # Re-read immediately before mutations.
instance_id = current["instance_id"]
if current["action"] == "reuse":
if current["instance_status"] != "Running":
raise CloudError(
"existing host is not Running; no automatic start/replace"
)
return current
if current["action"] == "create_and_bind":
params = create_params(cfg)
fingerprint = hashlib.sha256(
json.dumps([REGION, PUBLIC_IP, params], sort_keys=True).encode()
).hexdigest()
try:
state = (
json.loads(state_path.read_text()) if state_path.exists() else {}
)
except (OSError, ValueError) as exc:
raise CloudError(
"state file cannot be read; no creation allowed"
) from exc
if not isinstance(state, dict):
raise CloudError("invalid state; no creation allowed")
if state and state.get("fingerprint") != fingerprint:
raise CloudError(
"creation configuration changed; reconcile old resources/state before proceeding"
)
if not state:
state = {"fingerprint": fingerprint, "client_token": str(uuid.uuid4())}
save_state(state_path, state) # Persist BEFORE the billable request.
try:
uuid.UUID(state.get("client_token", ""))
except (ValueError, TypeError, AttributeError) as exc:
raise CloudError(
"invalid persisted ClientToken; reconcile state before creation"
) from exc
result = api(
"ecs", "RunInstances", ClientToken=state["client_token"], **params
)
ids = result.get("InstanceIdSets", {}).get("InstanceIdSet", [])
if len(ids) != 1:
raise CloudError(
"unexpected creation result; preserve ClientToken and reconcile"
)
instance_id = ids[0]
state["instance_id"] = instance_id
save_state(state_path, state)
wait_running(api, instance_id, sleep)
latest = eip(api)
if not latest or latest.get("AllocationId") != current["allocation_id"]:
raise CloudError(
"fixed EIP changed/disappeared; created host preserved, no substitute IP"
)
if latest.get("InstanceId") and latest["InstanceId"] != instance_id:
raise CloudError(
"EIP became attached to another instance; refusing to detach it"
)
if not latest.get("InstanceId"):
if latest.get("Status") != "Available":
raise CloudError(
"EIP is not safely bindable; preserve instance and retry later"
)
api(
"vpc",
"AssociateEipAddress",
AllocationId=latest["AllocationId"],
InstanceId=instance_id,
InstanceType="EcsInstance",
)
for _ in range(30):
bound = eip(api)
if (
bound
and bound.get("InstanceId") == instance_id
and bound.get("Status") == "InUse"
):
return dict(
current,
action="ready",
instance_id=instance_id,
instance_status="Running",
)
sleep(2)
raise CloudError(
"EIP binding not confirmed; preserve host/state and reconcile, do not allocate another address"
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", required=True)
parser.add_argument("--state", default=".local/aliyun-host.json")
parser.add_argument(
"--apply",
action="store_true",
help="owner-approved billable creation/EIP association",
)
args = parser.parse_args()
try:
cfg = json.loads(Path(args.config).read_text())
identity(cfg)
api = AliyunCLI(cfg.get("profile"))
result = apply(cfg, api, args.state) if args.apply else plan(cfg, api)
print(json.dumps(result, ensure_ascii=False, indent=2))
except (CloudError, OSError, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+8
View File
@@ -0,0 +1,8 @@
{
"public_ip": "123.56.71.98",
"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}
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Check-only by default. Run with 'up' explicitly after host/IP/config approval.
set -euo pipefail
cd "$(dirname "$0")/.."
action="${1:-check}"
case "$action" in check | up) ;; *)
echo 'usage: bash deploy/asterisk.sh [check|up]' >&2
exit 2
;;
esac
for file in http.conf ari.conf pjsip.conf rtp.conf extensions.conf; do
test -f "deploy/asterisk/generated/$file" || {
echo "Missing generated $file" >&2
exit 1
}
done
image="$(docker compose -f compose.asterisk.yaml config --format json |
python3 -c 'import json,sys; print(json.load(sys.stdin)["services"]["asterisk"]["image"])')"
if [[ ! "$image" =~ @sha256:[a-f0-9]{64}$ ]]; then
echo 'Asterisk image must be pinned to an approved SHA-256 digest' >&2
exit 1
fi
echo 'Compose, config files and image digest checked; verify container UID, network and fixed egress IP separately.'
if [[ "$action" == up ]]; then
docker compose -f compose.asterisk.yaml up -d
fi
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Render an explicit, outbound-only UDP baseline. Does not start services."""
import argparse
import ipaddress
import json
import os
import re
import shutil
import sys
import tempfile
from pathlib import Path
PUBLIC_IP = "123.56.71.98"
def scalar(value, name, secret=False):
if (
not isinstance(value, str)
or not value
or len(value) > 256
or any(c in value for c in "\r\n;[]\\")
):
raise ValueError(f"{name} is missing or contains unsupported INI characters")
if value.startswith("CHANGE_ME"):
raise ValueError(f"{name} is still a placeholder")
if secret and len(value) < 12:
raise ValueError(f"{name} must have at least 12 characters")
return value
def endpoint(name, data, env):
host = scalar(data.get("host"), name + ".host")
if (
not re.fullmatch(r"[A-Za-z0-9.-]+", host)
or host == PUBLIC_IP
or host.endswith((".invalid", ".example"))
):
raise ValueError(
f"{name}.host must be the SIP provider IPv4/DNS address, not our whitelisted IP or a placeholder"
)
port = data.get("port", 5060)
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
raise ValueError(f"{name}.port is invalid")
mode = data.get("auth_mode")
if mode not in ("ip", "digest"):
raise ValueError(f"{name}.auth_mode must be explicitly ip or digest")
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=ulaw\ndirect_media=no\nrtp_symmetric=yes\nforce_rport=yes\nrewrite_contact=yes\naors={name}-aor\n"
auth = ""
if mode == "digest":
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)
text += f"outbound_auth={name}-auth\nfrom_user={user}\n"
auth = f"\n[{name}-auth]\ntype=auth\nauth_type=userpass\nusername={user}\npassword={password}\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 += (
f"\n[{name}-aor]\ntype=aor\ncontact=sip:{host}:{port}\nqualify_frequency=10\n"
)
return text + auth
def render(cfg, env):
if cfg.get("public_ip") != PUBLIC_IP or cfg.get("transport") != "udp":
raise ValueError(
"fixed public IP and UDP baseline required; TCP/TLS need a reviewed configuration"
)
try:
network = ipaddress.ip_network(cfg["local_net"], strict=True)
bind = ipaddress.ip_address(cfg.get("ari_bind", "127.0.0.1"))
except (KeyError, ValueError) as exc:
raise ValueError(
"local_net and ARI bind address must be explicit valid IP configuration"
) from exc
if network.version != 4 or not network.is_private or network.prefixlen < 8:
raise ValueError(
"local_net must be the actual private IPv4 VPC network, not a default route"
)
if bind.version != 4 or bind.is_unspecified or not bind.is_private:
raise ValueError(
"ARI must bind to loopback or an approved private IPv4 management address"
)
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 = endpoint("provider-primary", cfg.get("primary", {}), env)
backup = endpoint("provider-backup", cfg.get("backup", {}), env)
if (cfg["primary"]["host"], cfg["primary"].get("port", 5060)) == (
cfg["backup"]["host"],
cfg["backup"].get("port", 5060),
):
raise ValueError(
"primary and backup targets must be distinct; shared failure domains still require validation"
)
transport = f"[global]\ntype=global\nuser_agent=ai-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"
return {
"http.conf": f"[general]\nenabled=yes\nbindaddr={bind}\nbindport=8088\n",
"ari.conf": f"[general]\nenabled=yes\npretty=no\n\n[outbound]\ntype=user\nread_only=no\npassword={password}\n",
"pjsip.conf": transport + primary + "\n" + backup,
"rtp.conf": "[general]\nrtpstart=10000\nrtpend=10800\nstrictrtp=yes\n",
"extensions.conf": "[deny-inbound]\nexten => s,1,Hangup()\nexten => _.,1,Hangup()\n",
}
def write_config(files, output):
output = Path(output)
if output.exists() or output.is_symlink():
raise ValueError(
"output already exists; use a new version directory and review the change before switching"
)
output.parent.mkdir(parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=".asterisk-render-", dir=output.parent))
try:
for name, content in files.items():
path = temporary / name
path.write_text(content)
path.chmod(0o600)
temporary.chmod(0o750)
temporary.rename(output)
except Exception:
shutil.rmtree(temporary)
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", required=True)
parser.add_argument("--output", default="deploy/asterisk/generated")
args = parser.parse_args()
try:
cfg = json.loads(Path(args.config).read_text())
files = render(cfg, os.environ)
write_config(files, args.output)
print(
f"Rendered {len(files)} configuration files to {args.output}; services NOT started. Check container UID permissions before deployment."
)
except (ValueError, OSError, KeyError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())