Require diagnostic provisioning in both installation paths
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
- 使用者批准独立的测试专用 `deploys/test/saas-mock/` 仅模拟缺失的 SaaS:从 0600 的静态快照提供五类正式 HTTP 配置,在专用 RabbitMQ vhost 中由模拟 SaaS 预建现行拓扑;不在 Dispatcher 内注入快照;默认不发布外呼消息,只有受限脚本已为同一 `event_id`/线路/原始号码启动抓包后,才可显式单次 MQ 投递;不替代 Agent/Asterisk/AI/OSS。测试用正式入口必须同时具备只读配置、专用 MQ、真实 Agent/Asterisk/AI/录音/OSS、逐通确认和拨号前活跃抓包证据,缺一项不得试拨。此模拟不构成真实 SaaS 签收。
|
||||
- 开发按 TDD 分批,小步提交;不得覆盖使用者现存修改/未跟踪文件,不自动清理、迁移或覆盖任何现存 SQLite、spool、outbox 和 Agent 恢复文件。旧 `.executions` 及恢复根目录中旧 `.uploads`、`.upload-locks`、逐执行 `state.json` 的发现须只读失败关闭,现存未交付事实由使用者确认处置。真实云账号、EIP、线路、拨号、生产部署和共享数据操作分别需要明确授权。
|
||||
|
||||
- 2026-10-08 使用者批准独立测试工具 [`cmd/sip-call`](cmd/sip-call/README.md):单独编译,以 `sip-call call --sip <线路.env> <原始号码>` 自动加载固定 ENV、临时配置并核对原生 Asterisk,仅拨一通,不接 SaaS/MQ/AI/OSS,不重拨、不换线;业务 Agent/其他 ARI 应用须停止,活动通话或旧测试配置阻断执行。仅 `--debug/-D` 才使用 tcpdump/tshark 收集本通证据;不带参数只报告原生通道事实,不编造 SIP/媒体/抓包事实。`sip-go-agent agent` 同样增加 `--debug/-D`,默认不依赖外部 tcpdump/抓包凭证;调试复用现有抓证脚本并在拨号前严格核对活跃凭证,不能静默降级。HEP 真实 SIP 响应、业务授权、时段、额度及未知执行保留规则不变。开发核验已完成;2026-10-08 经使用者另行确认,独立工具及按当前表生成的三份 0600 ENV 已部署到登记测试机 `rogee` 用户的 `~/sip-call/`,未更新业务 Agent/Dispatcher、未加载或改动既有 Asterisk 线路、未拨号或调用 AI。默认模式不依赖抓包工具;主机未安装 tshark,`-D` 抓证尚未就绪,不能静默降级。部署来源、文件 hash 和只读主机事实见 [`docs/evidence/sip-call-test-deployment-20261008.md`](docs/evidence/sip-call-test-deployment-20261008.md)。独立工具不属于生产发布制品,也不授权真实试拨。
|
||||
- 2026-10-08 使用者批准独立测试工具 [`cmd/sip-call`](cmd/sip-call/README.md):单独编译,以 `sip-call call --sip <线路.env> <原始号码>` 自动加载固定 ENV、临时配置并核对原生 Asterisk,仅拨一通,不接 SaaS/MQ/AI/OSS,不重拨、不换线;业务 Agent/其他 ARI 应用须停止,活动通话或旧测试配置阻断执行。仅 `--debug/-D` 才使用 tcpdump/tshark 收集本通证据;不带参数只报告原生通道事实,不编造 SIP/媒体/抓包事实。`sip-go-agent agent` 同样增加 `--debug/-D`,默认不依赖外部 tcpdump/抓包凭证;调试复用现有抓证脚本并在拨号前严格核对活跃凭证,不能静默降级。HEP 真实 SIP 响应、业务授权、时段、额度及未知执行保留规则不变。开发核验已完成;2026-10-08 经使用者另行确认,独立工具及按当前表生成的三份 0600 ENV 已部署到登记测试机 `rogee` 用户的 `~/sip-call/`,未更新业务 Agent/Dispatcher、未加载或改动既有 Asterisk 线路、未拨号或调用 AI。默认运行不调用抓包工具;使用者进一步要求生产和测试安装环境均统一预装 tcpdump/tshark,不按环境跳过,但抓包仍须显式 `--debug/-D`,不足时不能静默降级。登记测试机现已安装并核对 tcpdump 4.99.5、TShark 4.4.19 的 SIP/RTP 解析能力;未开启抓包或更改抓包权限,不能把工具安装当作逐通抓证、线路接通或生产签收。部署来源、文件 hash 和只读主机事实见 [`docs/evidence/sip-call-test-deployment-20261008.md`](docs/evidence/sip-call-test-deployment-20261008.md)。独立工具不属于生产发布制品,也不授权真实试拨。
|
||||
|
||||
## SaaS、Dispatcher 与 Agent 的现行边界
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ chmod 600 sip-xx.env
|
||||
./dist/sip-call call --sip sip-xx.env -D 18601010101
|
||||
```
|
||||
|
||||
`--debug` 与 `-D` 等价。默认模式不调用或检查 tcpdump/tshark;调试模式要求两者可执行、指定接口可抓包且权限充分。调试抓包未就绪或在 Dial 前已退出就不拨号。参数后的号码是原始号码,仅添加该文件声明的前缀一次;不要自己先加线路前缀。
|
||||
`--debug` 与 `-D` 等价。环境安装统一预装 tcpdump/tshark(生产与测试相同);预装不等于开启抓包。默认模式不调用或检查这些工具;调试模式要求两者可执行、指定接口可抓包且权限充分。调试抓包未就绪或在 Dial 前已退出就不拨号。参数后的号码是原始号码,仅添加该文件声明的前缀一次;不要自己先加线路前缀。
|
||||
|
||||
**命令会真实拨号。每次执行都须另获线路和号码的明确授权;本文示例不是授权。** 本次代码交付只做本地测试,未部署、未试拨。
|
||||
**命令会真实拨号。每次执行都须另获线路和号码的明确授权;本文示例不是授权。** 独立工具已另经使用者批准部署到登记测试机;没有执行真实试拨。
|
||||
|
||||
## 运行条件
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSharedContractIsOnlyRuntimeSource(t *testing.T) {
|
||||
if _, err := os.Stat("local"); !os.IsNotExist(err) {
|
||||
t.Fatal("contracts/local must not remain a parallel contract source")
|
||||
}
|
||||
out, err := exec.Command("git", "ls-files", "--stage", "--", "schema").CombinedOutput()
|
||||
if err != nil || !strings.HasPrefix(string(out), "160000 ") {
|
||||
t.Fatalf("contracts/schema must be a Git submodule: %s (%v)", out, err)
|
||||
}
|
||||
for _, name := range []string{"config-read.schema.json", "task-discovery.schema.json", "mq.schema.json", "mq.topology.json", "manifest.json"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
embedded, err := ReadCurrent(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shared, err := os.ReadFile("schema/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(embedded, shared) {
|
||||
t.Fatal("runtime contract differs from shared repository")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedEntrypointsReferenceCanonicalDefinitions(t *testing.T) {
|
||||
for _, name := range []string{"config-read.schema.json", "task-discovery.schema.json", "mq.schema.json"} {
|
||||
data, err := os.ReadFile("schema/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(data, []byte(".schema.json#")) {
|
||||
t.Fatalf("%s must reference canonical split schemas, not copy their definitions", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -34,9 +34,13 @@ tar -xzf sip-go-agent-<version>-linux-amd64.tar.gz
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The package contains only the two Go services, their systemd units, production
|
||||
environment templates, the endpoint inventory, the version lock and the
|
||||
installer. Credentials, certificates, broker URLs and the approved static Cell
|
||||
The package contains the two Go services, their systemd units, production
|
||||
environment templates, the endpoint inventory, the version lock, the installer,
|
||||
and `cell/install-diagnostics.sh`. Both production and test host installation
|
||||
always provision `tcpdump` and `tshark` through system packages, verify command
|
||||
versions and SIP/RTP decoders, and fail on errors. Tools being installed does
|
||||
not enable capture: `--debug/-D` remains explicit. No capture capabilities,
|
||||
user-group membership or sudo rules are added by this helper. Credentials, certificates, broker URLs and the approved static Cell
|
||||
artifact are injected separately. Asterisk is built or installed with the
|
||||
scripts under [`cell/`](cell/), and its management-owned configuration is never
|
||||
overwritten.
|
||||
|
||||
@@ -19,13 +19,14 @@ RELEASE_VERSION="$VERSION" "$ROOT/scripts/build-release.sh" "$RELEASE_DIR"
|
||||
mkdir -p -- "$(dirname -- "$ARCHIVE")"
|
||||
mkdir -- "$STAGE"
|
||||
cp -a "$RELEASE_DIR/." "$STAGE/"
|
||||
mkdir -p "$STAGE/systemd" "$STAGE/env" "$STAGE/config"
|
||||
mkdir -p "$STAGE/systemd" "$STAGE/env" "$STAGE/config" "$STAGE/cell"
|
||||
cp "$ROOT/deploys/install.sh" "$STAGE/install.sh"
|
||||
cp "$ROOT/deploys/cell/install-diagnostics.sh" "$STAGE/cell/install-diagnostics.sh"
|
||||
cp "$ROOT/deploys/systemd/"*.service "$STAGE/systemd/"
|
||||
cp "$ROOT/deploys/env/agent.env.example" "$ROOT/deploys/env/dispatcher.env.example" "$STAGE/env/"
|
||||
cp "$ROOT/deploys/config/agent-endpoints.example.json" "$STAGE/config/"
|
||||
cp "$LOCK" "$STAGE/versions.lock.json"
|
||||
chmod 0755 "$STAGE/install.sh"
|
||||
chmod 0755 "$STAGE/install.sh" "$STAGE/cell/install-diagnostics.sh"
|
||||
chmod 0644 "$STAGE/systemd/"*.service "$STAGE/env/"*.env.example "$STAGE/config/agent-endpoints.example.json" "$STAGE/versions.lock.json"
|
||||
(
|
||||
cd "$STAGE"
|
||||
|
||||
@@ -14,8 +14,15 @@ The pinned source input is:
|
||||
|
||||
`build-asterisk-native.sh` reproduces the stage from the pinned source and
|
||||
local dependency cache; the build selects no downloaded core sounds/MOH.
|
||||
The native package includes its `build-platform` marker and
|
||||
`install-asterisk-user.sh`. Run that installer as `rogee`, never as root;
|
||||
The native package includes its `build-platform` marker,
|
||||
`install-asterisk-user.sh`, and checksum-verified `install-diagnostics.sh`.
|
||||
Every production/test installation provisions `tcpdump` and `tshark` using the
|
||||
same root package helper; there is no environment switch or skip option.
|
||||
The native user installer invokes this helper through existing `sudo -n`
|
||||
administrative access after preflight validation and before changing Asterisk.
|
||||
The helper checks versions and SIP/RTP decoders, does not activate capture,
|
||||
and does not change capture permissions or user-group membership.
|
||||
Run the native installer as `rogee`, never as root;
|
||||
it verifies the stage hash and required libraries, installs into `~/.local/opt/`,
|
||||
sets up `~/.config/go-sip-asterisk/` without overwriting existing files, and
|
||||
installs `go-sip-asterisk.service` under `systemd --user`. Production requires
|
||||
|
||||
@@ -42,10 +42,12 @@ rm -rf -- "$STAGE/etc/asterisk"
|
||||
tar -C "$STAGE" -cpf "$OUT/asterisk-$VERSION-native-stage.tar" .
|
||||
cp "$ROOT/deploys/cell/install-asterisk-user.sh" "$OUT/install-asterisk-user.sh"
|
||||
cp "$ROOT/deploys/cell/native-platform.sh" "$OUT/native-platform.sh"
|
||||
cp "$ROOT/deploys/cell/install-diagnostics.sh" "$OUT/install-diagnostics.sh"
|
||||
printf 'debian:%s:x86_64\n' "$OS_VERSION" > "$OUT/build-platform"
|
||||
chmod 0755 "$OUT/install-asterisk-user.sh"
|
||||
chmod 0755 "$OUT/install-asterisk-user.sh" "$OUT/install-diagnostics.sh"
|
||||
(
|
||||
cd "$OUT"
|
||||
sha256sum "asterisk-$VERSION-native-stage.tar" > "asterisk-$VERSION-native-stage.tar.sha256"
|
||||
sha256sum install-diagnostics.sh > install-diagnostics.sh.sha256
|
||||
)
|
||||
printf 'native_stage=%s\nservice=%s\n' "$OUT/asterisk-$VERSION-native-stage.tar" "$OUT/asterisk.service"
|
||||
|
||||
@@ -27,6 +27,10 @@ runtime="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/go-sip-asterisk"
|
||||
[[ ! -e $prefix && ! -e $unit && ! -e $config/asterisk.conf && ! -e $config/modules.conf ]] || { echo 'existing Asterisk installation or user configuration; refuse to overwrite' >&2; exit 1; }
|
||||
[[ $HOME != *[[:space:]]* ]] || { echo 'home path with whitespace is unsupported by the unit' >&2; exit 1; }
|
||||
|
||||
# System tools are required for every environment, independently of --nonprod.
|
||||
(cd "$package" && sha256sum -c install-diagnostics.sh.sha256)
|
||||
sudo -n -- bash "$package/install-diagnostics.sh"
|
||||
|
||||
mkdir -p "$(dirname "$prefix")"
|
||||
staged=$(mktemp -d "$(dirname "$prefix")/.staged.XXXXXXXX")
|
||||
trap 'rm -rf "$staged"' EXIT
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Required on every host: tool availability does not enable packet capture.
|
||||
set -euo pipefail
|
||||
[[ $# == 0 ]] || { echo 'usage: install-diagnostics.sh (no environment switches)' >&2; exit 2; }
|
||||
[[ $EUID == 0 ]] || { echo 'diagnostic package installation requires root' >&2; exit 1; }
|
||||
for command in apt-get awk; do
|
||||
command -v "$command" >/dev/null || { echo "required installer command missing: $command" >&2; exit 1; }
|
||||
done
|
||||
# Noninteractive package defaults do not add users to the wireshark group or
|
||||
# grant capture capabilities. Existing operator capture permissions are retained.
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends tcpdump tshark
|
||||
for command in tcpdump tshark; do
|
||||
command -v "$command" >/dev/null || { echo "installed diagnostic command missing: $command" >&2; exit 1; }
|
||||
done
|
||||
tcpdump_version=$(tcpdump --version)
|
||||
tshark_version=$(tshark --version)
|
||||
printf '%s\n%s\n' "${tcpdump_version%%$'\n'*}" "${tshark_version%%$'\n'*}"
|
||||
protocols=$(tshark -G protocols)
|
||||
if ! awk -F '\t' '$3 == "sip" {sip=1} $3 == "rtp" {rtp=1} END {exit !(sip && rtp)}' <<< "$protocols"; then
|
||||
echo 'installed tshark lacks SIP/RTP dissectors' >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'tcpdump and tshark installed; SIP/RTP decoders verified; capture remains opt-in\n'
|
||||
@@ -37,6 +37,8 @@ LOCK_VERSION=$(awk -F'"' '/"version"[[:space:]]*:/ {print $4; exit}' versions.lo
|
||||
[[ "$VERSION" =~ ^[0-9A-Za-z][0-9A-Za-z.+_-]*$ ]] || { echo 'invalid release version' >&2; exit 1; }
|
||||
[[ "$VERSION" == "$LOCK_VERSION" ]] || { echo 'release/version lock mismatch' >&2; exit 1; }
|
||||
|
||||
bash "$PACKAGE_DIR/cell/install-diagnostics.sh"
|
||||
|
||||
id -u rogee >/dev/null 2>&1 || useradd --create-home --shell /bin/bash rogee
|
||||
install -d -m 0755 /opt/sip-go-agent/releases \
|
||||
/etc/sip-go-agent/pki /etc/sip-go-agent/artifacts
|
||||
|
||||
@@ -31,9 +31,15 @@ before installation. Without lingering, non-production start is session-scoped
|
||||
and reboot persistence must be reported as unverified.
|
||||
|
||||
The Go installer creates `/opt/sip-go-agent`, `/etc/sip-go-agent` and
|
||||
`/var/lib/sip-go-agent`, installs the two Go units, and enables them. It does
|
||||
not install test tooling, Docker, a broker, provider SDK credentials or AI
|
||||
snapshots. Start services only after the injected environment, mTLS identity,
|
||||
`/var/lib/sip-go-agent`, installs the two Go units, and enables them. It
|
||||
always installs `tcpdump` and `tshark` through the shared
|
||||
[`cell/install-diagnostics.sh`](cell/install-diagnostics.sh), for production and
|
||||
test hosts alike. The native Asterisk installer invokes the same helper with
|
||||
existing `sudo -n` administrative access before changing the installation.
|
||||
Versions and SIP/RTP dissectors are checked; package or validation failures
|
||||
abort installation. Capture still requires explicit `--debug/-D` and existing
|
||||
operator privileges. These tools are not business services. The installer does
|
||||
not install Docker, a broker, provider SDK credentials or AI snapshots. Start services only after the injected environment, mTLS identity,
|
||||
broker ACL and static Cell artifact have been reviewed.
|
||||
|
||||
For local or isolated testing, use the Docker-backed RabbitMQ target
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
|
||||
- 远端程序 `call --help` 运行通过,支持 `--sip`、`--debug/-D`。
|
||||
- 文件 hash、权限及目标目录验证通过;默认模式不依赖外部抓包工具。
|
||||
- 测试机已有 tcpdump,**没有 tshark**。本次未安装额外软件或改动抓包权限;`-D` 抓证尚未就绪,缺少工具时程序明确拒绝拨号,不静默降级。
|
||||
- 此次初始部署时测试机已有 tcpdump、尚无 tshark,未安装额外软件或改动抓包权限。随后使用者明确要求统一安装生产/测试诊断工具,tshark 已安装,详见 [`diagnostics-tools-install-20261008.md`](diagnostics-tools-install-20261008.md)。原始 `verification.json` 保留初始时间点事实,新的诊断工具核验另存,不覆盖历史。
|
||||
- 未执行任何 `call --sip ... <号码>`,没有本次工具发起的外呼,也没有本次采集的通话结果或抓包证据;不能据此次安装声称任一线路已正常接通。
|
||||
|
||||
@@ -20,6 +20,7 @@ if [[ -e scripts/mq-only-acceptance-local.sh ]]; then
|
||||
fi
|
||||
|
||||
./scripts/check-contracts.sh
|
||||
bash ./scripts/check-install-diagnostics.sh
|
||||
go mod verify
|
||||
go test -race ./... -count=1
|
||||
go vet ./...
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Root installer tests use fake package commands in a network-disabled container.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
helper="$ROOT/deploys/cell/install-diagnostics.sh"
|
||||
[[ -f "$helper" ]] || { echo 'diagnostic installer missing' >&2; exit 1; }
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir "$tmp/bin"
|
||||
cat > "$tmp/bin/apt-get" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s frontend=%s\n' "$*" "${DEBIAN_FRONTEND:-}" >> /fixture/apt.log
|
||||
if [ "$*" = update ] && [ "${FAIL_UPDATE:-0}" = 1 ]; then exit 41; fi
|
||||
if [ "${1:-}" = install ] && [ "${FAIL_INSTALL:-0}" = 1 ]; then exit 42; fi
|
||||
SH
|
||||
cat > "$tmp/bin/tcpdump" <<'SH'
|
||||
#!/bin/sh
|
||||
[ "${FAIL_TCPDUMP:-0}" = 0 ] || exit 43
|
||||
printf 'tcpdump version fixture\n'
|
||||
SH
|
||||
cat > "$tmp/bin/tshark" <<'SH'
|
||||
#!/bin/sh
|
||||
[ "${FAIL_TSHARK:-0}" = 0 ] || exit 44
|
||||
case "$*" in
|
||||
--version) printf 'TShark fixture\n' ;;
|
||||
'-G protocols')
|
||||
printf 'Session Initiation Protocol\tSIP\tsip\n'
|
||||
[ "${MISSING_RTP:-0}" = 0 ] && printf 'Real-Time Transport Protocol\tRTP\trtp\n'
|
||||
exit 0 ;;
|
||||
*) exit 45 ;;
|
||||
esac
|
||||
SH
|
||||
chmod 755 "$tmp/bin/"*
|
||||
cat > "$tmp/check.sh" <<'SH'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
export PATH=/fixture/bin:/usr/bin:/bin
|
||||
for environment in production development test; do
|
||||
rm -f /fixture/apt.log
|
||||
AGENT_ENVIRONMENT="$environment" /bin/bash /install-diagnostics.sh
|
||||
grep -Fx 'update frontend=noninteractive' /fixture/apt.log
|
||||
grep -Fx 'install -y --no-install-recommends tcpdump tshark frontend=noninteractive' /fixture/apt.log
|
||||
done
|
||||
for failure in FAIL_UPDATE FAIL_INSTALL FAIL_TCPDUMP FAIL_TSHARK MISSING_RTP; do
|
||||
if env "$failure=1" /bin/bash /install-diagnostics.sh; then
|
||||
echo "installer swallowed failure: $failure" >&2; exit 1
|
||||
fi
|
||||
done
|
||||
if /bin/bash /install-diagnostics.sh --nonprod; then echo 'installer accepted environment switch' >&2; exit 1; fi
|
||||
printf 'diagnostic installer root checks passed\n'
|
||||
SH
|
||||
chmod 755 "$tmp/check.sh"
|
||||
docker run --rm --network none -v "$tmp:/fixture" -v "$helper:/install-diagnostics.sh:ro" debian:13-slim /bin/bash /fixture/check.sh
|
||||
if [[ $EUID != 0 ]] && bash "$helper"; then echo 'installer accepted unprivileged execution' >&2; exit 1; fi
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import pathlib, sys
|
||||
root=pathlib.Path(sys.argv[1])
|
||||
production=(root/'deploys/install.sh').read_text()
|
||||
native=(root/'deploys/cell/install-asterisk-user.sh').read_text()
|
||||
package=(root/'deploys/build-package.sh').read_text()
|
||||
build=(root/'deploys/cell/build-asterisk-native.sh').read_text()
|
||||
assert 'cell/install-diagnostics.sh' in production, 'production installer does not provision diagnostics'
|
||||
assert 'install-diagnostics.sh' in native and native.index('install-diagnostics.sh') < native.index('mkdir -p "$(dirname "$prefix")"'), 'native installer must provision diagnostics before changing Asterisk'
|
||||
assert 'cell/install-diagnostics.sh' in package, 'production package omits helper'
|
||||
assert 'install-diagnostics.sh' in build, 'native package omits helper'
|
||||
print('both installation paths include unconditional diagnostic provisioning')
|
||||
PY
|
||||
Reference in New Issue
Block a user