refactor!: rename the project from Monitor to MoShiro

Namespaces, assemblies, projects and directories, the solution, binaries,
images, systemd units, config paths, environment variables, protocol method
names, the media type and the raw chunk magic all move to MoShiro/moshiro.
Existing data and deployments are not compatible and must be recreated.
Also fix the architecture tests after the solution layout change and make
the update tests independent of their order.
This commit is contained in:
justme committed 2026-10-07 01:49:04 +08:00
1 parent 17b35223aa
commit 83e5120377
309 files changed
+1505 -1499

No files matched your search

+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# 打包:scripts/package.sh <agent|probe> <rid> → backend/artifacts/packages/monitor-<role>-<version>-<rid>.tar.gz(附 .sha256)
# 打包:scripts/package.sh <agent|probe> <rid> → backend/artifacts/packages/moshiro-<role>-<version>-<rid>.tar.gz(附 .sha256)
# 内容:节点二进制(agent 还带 fleet-enroll)、systemd 单元、OpenWrt procd 脚本、install.sh。
# 发布产物要先用 scripts/publish.sh 生成(agent 需要 agent 和 fleet-enroll 两个)。
set -euo pipefail
@@ -7,14 +7,14 @@ role="${1:?usage: package.sh <agent|probe> <rid>}"
rid="${2:?usage: package.sh <agent|probe> <rid>}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
version="$(sed -n 's#.*<Version>\(.*\)</Version>.*#\1#p' "$root/Directory.Build.props" | head -1)"
name="monitor-$role-$version-$rid"
name="moshiro-$role-$version-$rid"
stage="$root/backend/artifacts/packages/stage/$name"
rm -rf "$stage"
mkdir -p "$stage/systemd"
cp "$root/backend/artifacts/publish/$role/$rid/monitor-$role" "$stage/"
cp "$root/backend/artifacts/publish/$role/$rid/moshiro-$role" "$stage/"
cp "$root/packaging/install.sh" "$stage/"
cp "$root/packaging/systemd/monitor-$role.service" "$stage/systemd/"
cp "$root/packaging/systemd/moshiro-$role.service" "$stage/systemd/"
cp "$root/$role/README.md" "$stage/README.md"
if [ "$role" = agent ]; then
cp "$root/backend/artifacts/publish/fleet-enroll/$rid/fleet-enroll" "$stage/"
+9 -9
View File
@@ -9,18 +9,18 @@ rid="${2:?usage: publish.sh <agent|probe|backend|fleet-enroll> <rid>}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
case "$project" in
agent) csproj="backend/agent/src/Monitor.Agent" ;;
probe) csproj="backend/probe/src/Monitor.Probe" ;;
backend) csproj="backend/src/Monitor.Backend" ;;
agent) csproj="backend/agent/src/MoShiro.Agent" ;;
probe) csproj="backend/probe/src/MoShiro.Probe" ;;
backend) csproj="backend/src/MoShiro.Backend" ;;
fleet-enroll) csproj="backend/fleet/tools/fleet-enroll" ;;
*) echo "unknown project: $project" >&2; exit 2 ;;
esac
# 构建信息(程序里显示的 tag、提交、构建时间):没在 git 仓库里就留空
export MONITOR_TAG="${MONITOR_TAG:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || true)}"
export MONITOR_COMMIT="${MONITOR_COMMIT:-$(git -C "$root" rev-parse --short=10 HEAD 2>/dev/null || true)}"
export MONITOR_BUILT_AT="${MONITOR_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
build_env=(-e MONITOR_TAG -e MONITOR_COMMIT -e MONITOR_BUILT_AT)
export MOSHIRO_TAG="${MOSHIRO_TAG:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || true)}"
export MOSHIRO_COMMIT="${MOSHIRO_COMMIT:-$(git -C "$root" rev-parse --short=10 HEAD 2>/dev/null || true)}"
export MOSHIRO_BUILT_AT="${MOSHIRO_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
build_env=(-e MOSHIRO_TAG -e MOSHIRO_COMMIT -e MOSHIRO_BUILT_AT)
out="backend/artifacts/publish/$project/$rid"
publish_options=()
@@ -32,11 +32,11 @@ args=(publish "$csproj" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=non
case "$rid" in
linux-musl-x64)
# SDK 镜像的版本可以比 global.json 新(rollForward = latestFeature)
docker run --rm "${build_env[@]}" -v "$root:/src" -v monitor-nuget:/root/.nuget -v "monitor-artifacts-$rid:/artifacts" -w /src \
docker run --rm "${build_env[@]}" -v "$root:/src" -v moshiro-nuget:/root/.nuget -v "moshiro-artifacts-$rid:/artifacts" -w /src \
mcr.microsoft.com/dotnet/sdk:10.0-alpine-aot dotnet "${args[@]}"
;;
linux-x64)
docker run --rm "${build_env[@]}" -v "$root:/src" -v monitor-nuget:/root/.nuget -v "monitor-artifacts-$rid:/artifacts" -w /src mcr.microsoft.com/dotnet/sdk:10.0-noble-aot dotnet "${args[@]}"
docker run --rm "${build_env[@]}" -v "$root:/src" -v moshiro-nuget:/root/.nuget -v "moshiro-artifacts-$rid:/artifacts" -w /src mcr.microsoft.com/dotnet/sdk:10.0-noble-aot dotnet "${args[@]}"
;;
linux-musl-arm64|linux-arm64)
echo "$rid needs an arm64 Alpine builder (native or qemu): run this script on an arm64 host or in CI" >&2
+14 -14
View File
@@ -3,7 +3,7 @@
#
# RID=linux-musl-x64 IMAGE=alpine:3.22 (默认)
# RID=linux-x64 IMAGE=debian:bookworm-slim
# MODE=install 用打包产物里的 install.sh 安装(agent 注册、probe 身份、以 monitor 用户运行),而不是直接运行二进制
# MODE=install 用打包产物里的 install.sh 安装(agent 注册、probe 身份、以 moshiro 用户运行),而不是直接运行二进制
#
# 用法(在仓库根目录;Windows 下先 export MSYS_NO_PATHCONV=1;MODE=install 需要先运行 scripts/package.sh):
# docker run --rm -e RID -e MODE -v "$PWD/backend/artifacts:/art:ro" -v "$PWD/scripts/smoke-linux.sh:/smoke.sh:ro" $IMAGE sh /smoke.sh [秒数]
@@ -23,9 +23,9 @@ work=/tmp/smoke
mkdir -p "$work/data" "$work/probe"
base=http://127.0.0.1:18080
export ASPNETCORE_URLS="$base" Monitor__DataDirectory="$work/data" Monitor__InitialAdminUser=admin \
Monitor__InitialAdminPassword="correct horse battery" Monitor__SecureCookies=false
(cd "$pub/backend/$rid" && ./monitor-backend >"$work/backend.log" 2>&1 &)
export ASPNETCORE_URLS="$base" MoShiro__DataDirectory="$work/data" MoShiro__InitialAdminUser=admin \
MoShiro__InitialAdminPassword="correct horse battery" MoShiro__SecureCookies=false
(cd "$pub/backend/$rid" && ./moshiro-backend >"$work/backend.log" 2>&1 &)
for i in $(seq 1 60); do curl -fs "$base/healthz" >/dev/null 2>&1 && break; sleep 1; done
curl -fs "$base/healthz" >/dev/null || { cat "$work/backend.log"; echo "backend did not start"; exit 1; }
echo "backend up"
@@ -38,27 +38,27 @@ auth="Authorization: Bearer $token"
join=$(curl -fs -X POST "$base/api/admin/join-tokens" -H "$auth" -H 'Content-Type: application/json' -d '{"auto_approve":true,"max_uses":1,"expires_in_hours":1}' | jq -r .plaintext)
if [ "$mode" = install ]; then
# 安装到 /usr/lib/monitor 和 /etc/monitor,以 monitor 用户运行(没有 systemd:自己启动)
mkdir -p /tmp/pkg && for role in agent probe; do tar -C /tmp/pkg -xzf /art/packages/monitor-$role-*-$rid.tar.gz; done
(cd /tmp/pkg/monitor-agent-* && ./install.sh agent --server "$base" --join "$join" --name smoke-agent --no-start)
ls -l /usr/lib/monitor /etc/monitor
run_as() { su -s /bin/sh monitor -c "exec $*"; }
run_as /usr/lib/monitor/monitor-agent --config /etc/monitor/agent.json >"$work/agent.log" 2>&1 &
# 安装到 /usr/lib/moshiro 和 /etc/moshiro,以 moshiro 用户运行(没有 systemd:自己启动)
mkdir -p /tmp/pkg && for role in agent probe; do tar -C /tmp/pkg -xzf /art/packages/moshiro-$role-*-$rid.tar.gz; done
(cd /tmp/pkg/moshiro-agent-* && ./install.sh agent --server "$base" --join "$join" --name smoke-agent --no-start)
ls -l /usr/lib/moshiro /etc/moshiro
run_as() { su -s /bin/sh moshiro -c "exec $*"; }
run_as /usr/lib/moshiro/moshiro-agent --config /etc/moshiro/agent.json >"$work/agent.log" 2>&1 &
agent_pid=$!
sleep 3
curl -fs -X POST "$base/api/admin/probe/identity" -H "$auth" -H 'Content-Type: application/json' -d '{}' >"$work/probe/probe.json"
(cd /tmp/pkg/monitor-probe-* && ./install.sh probe --probe-json "$work/probe/probe.json" --no-start)
run_as /usr/lib/monitor/monitor-probe --config /etc/monitor/probe.json >"$work/probe.log" 2>&1 &
(cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --probe-json "$work/probe/probe.json" --no-start)
run_as /usr/lib/moshiro/moshiro-probe --config /etc/moshiro/probe.json >"$work/probe.log" 2>&1 &
probe_pid=$!
else
mkdir -p "$work/agent-id"
"$pub/fleet-enroll/$rid/fleet-enroll" --server "$base" --join "$join" --name smoke-agent --identity-dir "$work/agent-id"
printf '{"fleet":{"server":"%s","identity_dir":"%s"},"listen":{"bind":"127.0.0.1"}}' "$base" "$work/agent-id" >"$work/agent.json"
"$pub/agent/$rid/monitor-agent" --config "$work/agent.json" >"$work/agent.log" 2>&1 &
"$pub/agent/$rid/moshiro-agent" --config "$work/agent.json" >"$work/agent.log" 2>&1 &
agent_pid=$!
sleep 3
curl -fs -X POST "$base/api/admin/probe/identity" -H "$auth" -H 'Content-Type: application/json' -d '{}' >"$work/probe/probe.json"
"$pub/probe/$rid/monitor-probe" --config "$work/probe/probe.json" >"$work/probe.log" 2>&1 &
"$pub/probe/$rid/moshiro-probe" --config "$work/probe/probe.json" >"$work/probe.log" 2>&1 &
probe_pid=$!
fi
+11 -11
View File
@@ -6,7 +6,7 @@ apt-get update >/dev/null && apt-get install -y --no-install-recommends curl jq
base=http://127.0.0.1:18080
mkdir -p /tmp/data
cd /art/publish/backend/linux-x64
ASPNETCORE_URLS=$base Monitor__DataDirectory=/tmp/data Monitor__InitialAdminUser=admin Monitor__InitialAdminPassword="correct horse battery" Monitor__SecureCookies=false nohup ./monitor-backend >/tmp/backend.log 2>&1 &
ASPNETCORE_URLS=$base MoShiro__DataDirectory=/tmp/data MoShiro__InitialAdminUser=admin MoShiro__InitialAdminPassword="correct horse battery" MoShiro__SecureCookies=false nohup ./moshiro-backend >/tmp/backend.log 2>&1 &
for i in $(seq 1 60); do curl -fs $base/healthz >/dev/null 2>&1 && break; sleep 1; done
login=$(curl -si -X POST $base/api/admin/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"correct horse battery"}')
cookie=$(echo "$login" | grep -i '^set-cookie' | head -1 | sed 's/^[^:]*: //; s/;.*//' | tr -d '\r')
@@ -14,18 +14,18 @@ csrf=$(echo "$login" | tail -1 | jq -r .csrf_token)
token=$(curl -fs -X POST $base/api/admin/api-tokens -H "Cookie: $cookie" -H "X-CSRF-Token: $csrf" -H 'Content-Type: application/json' -d '{"name":"t","scope":"admin"}' | jq -r .plaintext)
auth="Authorization: Bearer $token"
join=$(curl -fs -X POST $base/api/admin/join-tokens -H "$auth" -H 'Content-Type: application/json' -d '{"auto_approve":true,"max_uses":1,"expires_in_hours":1}' | jq -r .plaintext)
mkdir -p /tmp/pkg; for r in agent probe; do tar -C /tmp/pkg -xzf /art/packages/monitor-$r-*-linux-x64.tar.gz; done
(cd /tmp/pkg/monitor-agent-* && ./install.sh agent --server $base --join "$join" --name sysd-agent)
mkdir -p /tmp/pkg; for r in agent probe; do tar -C /tmp/pkg -xzf /art/packages/moshiro-$r-*-linux-x64.tar.gz; done
(cd /tmp/pkg/moshiro-agent-* && ./install.sh agent --server $base --join "$join" --name sysd-agent)
sleep 3
curl -fs -X POST $base/api/admin/probe/identity -H "$auth" -H 'Content-Type: application/json' -d '{}' >/tmp/probe.json
(cd /tmp/pkg/monitor-probe-* && ./install.sh probe --probe-json /tmp/probe.json)
(cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --probe-json /tmp/probe.json)
sleep 25
systemctl is-active monitor-agent monitor-probe
systemctl is-active moshiro-agent moshiro-probe
echo "agents: $(curl -fs $base/api/admin/agents -H "$auth" | jq -c '[.items[] | {name, status, probe_connected}]')"
echo "chunks: $(curl -fs $base/api/admin/raw/chunks -H "$auth" | jq -c '[.items[].kind] | length')"
systemctl show monitor-agent -p MemoryCurrent -p NRestarts | tr '\n' ' '; systemctl show monitor-probe -p MemoryCurrent -p NRestarts | tr '\n' ' '; echo
ps -o user,rss,comm -C monitor-agent,monitor-probe
journalctl -u monitor-agent -u monitor-probe --no-pager | tail -8
echo "--- sandbox check: agent cannot write /etc/monitor"; nsenter -t $(systemctl show monitor-agent -p MainPID --value) -m sh -c 'touch /etc/monitor/x 2>&1; ls /proc/self/status >/dev/null' 2>&1 | head -2
grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show monitor-agent -p MainPID --value)/status
grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show monitor-probe -p MainPID --value)/status
systemctl show moshiro-agent -p MemoryCurrent -p NRestarts | tr '\n' ' '; systemctl show moshiro-probe -p MemoryCurrent -p NRestarts | tr '\n' ' '; echo
ps -o user,rss,comm -C moshiro-agent,moshiro-probe
journalctl -u moshiro-agent -u moshiro-probe --no-pager | tail -8
echo "--- sandbox check: agent cannot write /etc/moshiro"; nsenter -t $(systemctl show moshiro-agent -p MainPID --value) -m sh -c 'touch /etc/moshiro/x 2>&1; ls /proc/self/status >/dev/null' 2>&1 | head -2
grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show moshiro-agent -p MainPID --value)/status
grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show moshiro-probe -p MainPID --value)/status
+7 -7
View File
@@ -1,8 +1,8 @@
"""跨机器的真实链路验证:Windows 上的 Probe(NativeAOT exe)→ WSL2 里的 Agent 和 Backend(真实的虚拟网络、不同的单调时钟原点)。
前提:WSL 里有 Debian(root)、tc(netem 注入丢包/延迟/黑洞用);已经用 scripts/publish.sh 发布了 linux-x64 的 agent、backend、fleet-enroll,
并且 Windows 上发布了 probe(dotnet publish backend/probe/src/Monitor.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。
把 linux-x64 的产物复制到 WSL 的 /root/mon/,在 WSL 里用 setsid -f nohup 启动 monitor-backend(监听 0.0.0.0:29181),再运行本脚本。
并且 Windows 上发布了 probe(dotnet publish backend/probe/src/MoShiro.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。
把 linux-x64 的产物复制到 WSL 的 /root/mon/,在 WSL 里用 setsid -f nohup 启动 moshiro-backend(监听 0.0.0.0:29181),再运行本脚本。
故障注入见 docs/deployment.md 第 3 节。
"""
import json, os, subprocess, sys, time, urllib.request, urllib.error
@@ -10,8 +10,8 @@ import json, os, subprocess, sys, time, urllib.request, urllib.error
# 要连的是本机的虚拟网络(WSL),不能走环境变量里的 HTTP(S)_PROXY
OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
ROOT = r"C:\Users\JustMe\Desktop\monitor-proj"
PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "monitor-probe.exe")
ROOT = r"C:\Users\JustMe\Desktop\moshiro-proj"
PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "moshiro-probe.exe")
WORK = r"C:\Users\JustMe\AppData\Local\Temp\realnet"
PORT = 29181
@@ -45,18 +45,18 @@ def req(method, path, body=None, headers=None, ctype="application/json"):
def main():
os.makedirs(WORK, exist_ok=True)
print("wsl ip", WSLIP)
resp, login = req("POST", "/api/admin/auth/login", {"username": "admin", "password": os.environ.get("MONITOR_ADMIN_PASSWORD", "correct horse battery")})
resp, login = req("POST", "/api/admin/auth/login", {"username": "admin", "password": os.environ.get("MOSHIRO_ADMIN_PASSWORD", "correct horse battery")})
cookie = resp.headers["Set-Cookie"].split(";")[0]
_, tok = req("POST", "/api/admin/api-tokens", {"name": "net", "scope": "admin"}, {"Cookie": cookie, "X-CSRF-Token": login["csrf_token"]})
auth = {"Authorization": "Bearer " + tok["plaintext"]}
json.dump({"base": BASE, "token": tok["plaintext"], "wslip": WSLIP}, open(WORK + r"\state.json", "w"))
_, join = req("POST", "/api/admin/join-tokens", {"auto_approve": True, "max_uses": 1, "expires_in_hours": 1}, auth)
wsl("pkill monitor-agent; cd /root/mon && rm -rf id agent.json")
wsl("pkill moshiro-agent; cd /root/mon && rm -rf id agent.json")
print(wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join {join['plaintext']} --name wsl-agent --identity-dir /root/mon/id"))
cfg = {"fleet": {"server": f"http://127.0.0.1:{PORT}", "identity_dir": "/root/mon/id"}, "listen": {"bind": "0.0.0.0"}}
wsl("cat > /root/mon/agent.json <<'EOF'\n" + json.dumps(cfg) + "\nEOF")
wsl("cd /root/mon && (setsid nohup ./monitor-agent --config /root/mon/agent.json > agent.log 2>&1 < /dev/null &)")
wsl("cd /root/mon && (setsid nohup ./moshiro-agent --config /root/mon/agent.json > agent.log 2>&1 < /dev/null &)")
time.sleep(3)
_, agents = req("GET", "/api/admin/agents", None, auth)
agent = agents["items"][0]
+31 -31
View File
@@ -5,7 +5,7 @@
Probe↔Agent 走真实的虚拟网络(eth0),Agent↔Backend 走 WSL 里的回环(故障用 tc 注入在 lo 上)。
前提:WSL 里有 Debian(root)和 tc;已发布 linux-x64 的 agent、backend、fleet-enroll(scripts/publish.sh),
以及 Windows 的 probe(dotnet publish backend/probe/src/Monitor.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。
以及 Windows 的 probe(dotnet publish backend/probe/src/MoShiro.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。
用法:python scripts/stability_wsl.py [场景名 ...] (不带参数运行全部)
场景:init graceful crash probe_link backend_link silent soak
@@ -23,11 +23,11 @@ import urllib.request
OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "monitor-probe.exe")
PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "moshiro-probe.exe")
PUB = "/mnt/" + ROOT[0].lower() + ROOT[2:].replace("\\", "/") + "/backend/artifacts/publish"
WORK = os.path.join(os.environ.get("TEMP", "."), "stability")
PORT = 29181
ADMIN_PASSWORD = os.environ.get("MONITOR_ADMIN_PASSWORD", "correct horse battery") # 测试用后端的初始管理员密码
ADMIN_PASSWORD = os.environ.get("MOSHIRO_ADMIN_PASSWORD", "correct horse battery") # 测试用后端的初始管理员密码
RESULTS = []
HIDDEN = subprocess.CREATE_NO_WINDOW # 所有子进程都不弹窗
@@ -109,14 +109,14 @@ def rss_mb(name):
def start_backend():
wsl(f"cd /root/mon && /root/mon/start.sh backend env ASPNETCORE_URLS=http://0.0.0.0:{PORT} Monitor__DataDirectory=/root/mon/data "
f"Monitor__InitialAdminUser=admin 'Monitor__InitialAdminPassword={ADMIN_PASSWORD}' Monitor__SecureCookies=false ./monitor-backend")
wsl(f"cd /root/mon && /root/mon/start.sh backend env ASPNETCORE_URLS=http://0.0.0.0:{PORT} MoShiro__DataDirectory=/root/mon/data "
f"MoShiro__InitialAdminUser=admin 'MoShiro__InitialAdminPassword={ADMIN_PASSWORD}' MoShiro__SecureCookies=false ./moshiro-backend")
return wait_until(backend_up, 30)
def start_agent():
wsl("/root/mon/start.sh agent ./monitor-agent --config /root/mon/agent.json")
return wait_until(lambda: bool(pids("monitor-agent")), 10)
wsl("/root/mon/start.sh agent ./moshiro-agent --config /root/mon/agent.json")
return wait_until(lambda: bool(pids("moshiro-agent")), 10)
def stop_proc(name, sig="TERM", timeout=30):
@@ -124,7 +124,7 @@ def stop_proc(name, sig="TERM", timeout=30):
start = now()
wsl(f"pkill -{sig} -x {name} || true")
gone = wait_until(lambda: not pids(name), timeout, 0.2)
code = wsl(f"cat /root/mon/{name.replace('monitor-', '')}.exit 2>/dev/null || true")
code = wsl(f"cat /root/mon/{name.replace('moshiro-', '')}.exit 2>/dev/null || true")
return (now() - start if gone is not None else None), (int(code) if code.strip().lstrip('-').isdigit() else None)
@@ -209,9 +209,9 @@ def setup(fresh=True):
"""Clean state, copy the artifacts, start backend/agent, create the probe identity. The probe is not started."""
global TOKEN, AGENT_ID
PROBE.stop("kill")
wsl("pkill -9 -x monitor-agent; pkill -9 -x monitor-backend; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev lo root 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true")
wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev lo root 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true")
if fresh:
wsl(f"rm -rf /root/mon && mkdir -p /root/mon && cp {PUB}/backend/linux-x64/* {PUB}/agent/linux-x64/* {PUB}/fleet-enroll/linux-x64/* /root/mon/ && chmod +x /root/mon/monitor-* /root/mon/fleet-enroll")
wsl(f"rm -rf /root/mon && mkdir -p /root/mon && cp {PUB}/backend/linux-x64/* {PUB}/agent/linux-x64/* {PUB}/fleet-enroll/linux-x64/* /root/mon/ && chmod +x /root/mon/moshiro-* /root/mon/fleet-enroll")
wsl("cat > /root/mon/start.sh <<'EOF'\n" + LAUNCHER + "EOF\nchmod +x /root/mon/start.sh")
if os.path.isdir(WORK):
subprocess.run(["cmd", "/c", "rmdir", "/s", "/q", WORK], capture_output=True, creationflags=HIDDEN)
@@ -279,7 +279,7 @@ def healthy():
def ensure_healthy(timeout=60):
if not backend_up():
start_backend()
if not pids("monitor-agent"):
if not pids("moshiro-agent"):
start_agent()
if not PROBE.alive():
PROBE.start()
@@ -292,7 +292,7 @@ def scenario_init():
print("== init: cold starts in every order, bad configuration")
# 1. everything down, then start in the opposite order: probe -> agent -> backend (each must retry until its peer is up)
PROBE.stop("kill")
wsl("pkill -9 -x monitor-agent; pkill -9 -x monitor-backend; true")
wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; true")
time.sleep(1)
PROBE.start()
time.sleep(3)
@@ -307,7 +307,7 @@ def scenario_init():
# 2. backend first, then agent, then probe (the normal order) after a full stop
PROBE.stop("kill")
wsl("pkill -9 -x monitor-agent; pkill -9 -x monitor-backend; true")
wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; true")
start_backend()
start_agent()
PROBE.start()
@@ -322,9 +322,9 @@ def scenario_init():
check("probe: malformed config exits 1 with a readable error", r.returncode == 1 and "startup failed" in r.stderr + r.stdout, (r.stderr + r.stdout).strip()[:120])
r = subprocess.run([PROBE_EXE, "--config", os.path.join(WORK, "missing.json")], capture_output=True, text=True, timeout=20, creationflags=HIDDEN)
check("probe: missing config exits 1 with the file name", r.returncode == 1 and "missing.json" in r.stderr + r.stdout, (r.stderr + r.stdout).strip()[:120])
out = wsl("cd /root/mon && echo '{ not json' > bad.json && ./monitor-agent --config bad.json 2>&1; echo exit=$?", check=False)
out = wsl("cd /root/mon && echo '{ not json' > bad.json && ./moshiro-agent --config bad.json 2>&1; echo exit=$?", check=False)
check("agent: malformed config exits 1", "exit=1" in out and "startup failed" in out, out.strip()[-120:])
out = wsl("cd /root/mon && printf '{\"fleet\":{\"server\":\"http://127.0.0.1:1\",\"identity_dir\":\"/nonexistent\"}}' > noid.json && ./monitor-agent --config noid.json 2>&1; echo exit=$?", check=False)
out = wsl("cd /root/mon && printf '{\"fleet\":{\"server\":\"http://127.0.0.1:1\",\"identity_dir\":\"/nonexistent\"}}' > noid.json && ./moshiro-agent --config noid.json 2>&1; echo exit=$?", check=False)
check("agent: no identity says to run fleet-enroll first", "exit=1" in out and "fleet-enroll" in out, out.strip()[-140:])
out = wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join bogus --name x --identity-dir /tmp/enroll-x 2>&1; echo exit=$?", check=False)
check("fleet-enroll: a bad join token fails clearly (non-zero exit)", "exit=0" not in out, out.strip()[-140:])
@@ -345,7 +345,7 @@ def scenario_graceful():
check("probe restarts and reconnects", wait_until(healthy, 30) is not None)
# agent
secs, code = stop_proc("monitor-agent", "TERM")
secs, code = stop_proc("moshiro-agent", "TERM")
log = wsl("tail -5 /root/mon/agent.log")
check("agent exits on SIGTERM within 5 s with code 0", secs is not None and secs < 5 and code == 0, f"{secs and round(secs, 2)}s code={code}")
check("agent logs a clean stop (no crash trace)", "stopping" in log and "Unhandled" not in log, log.splitlines()[-1][:100] if log else "")
@@ -355,7 +355,7 @@ def scenario_graceful():
check("agent restarts, reconnects to backend and probe", wait_until(healthy, 40) is not None)
# backend
secs, code = stop_proc("monitor-backend", "TERM", 30)
secs, code = stop_proc("moshiro-backend", "TERM", 30)
log = wsl("tail -5 /root/mon/backend.log")
check("backend exits on SIGTERM within 15 s with code 0", secs is not None and secs < 15 and code == 0, f"{secs and round(secs, 2)}s code={code}")
check("backend log has no unhandled exception", "Unhandled" not in log and "Exception" not in log, log.replace(chr(10), ' | ')[-160:])
@@ -373,7 +373,7 @@ def scenario_crash():
# backend crash: the agent keeps its outbox; after restart nothing acknowledged is lost
t = now()
wsl("pkill -9 -x monitor-backend")
wsl("pkill -9 -x moshiro-backend")
time.sleep(25) # agent outbox accumulates (2-3 chunk rounds)
start_backend()
back = wait_until(healthy, 60)
@@ -384,7 +384,7 @@ def scenario_crash():
check("backend kill -9: chunks buffered by the agent are delivered after the restart (no gap)", not g, f"gaps in {n} buckets: {g}; chunks {before}->{after}")
# agent crash
wsl("pkill -9 -x monitor-agent")
wsl("pkill -9 -x moshiro-agent")
time.sleep(5)
start_agent()
check("agent kill -9: recovers", wait_until(healthy, 40) is not None)
@@ -435,7 +435,7 @@ def scenario_probe_link():
print("== probe<->agent link: flapping, long blackhole (half-open), reconnect storms")
ensure_healthy()
time.sleep(20)
mem0 = (PROBE.working_set_mb(), rss_mb("monitor-agent"))
mem0 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"))
# 1. flapping: 3 s blackholes every 10 s, 8 times
for _ in range(8):
@@ -458,7 +458,7 @@ def scenario_probe_link():
# 3. reconnect storm: kill the agent 12 times in a row
for _ in range(12):
wsl("pkill -9 -x monitor-agent; true")
wsl("pkill -9 -x moshiro-agent; true")
time.sleep(0.5)
start_agent()
time.sleep(2)
@@ -475,7 +475,7 @@ def scenario_probe_link():
time.sleep(2)
check("8 probe restarts in a row: converges", wait_until(healthy, 60) is not None)
time.sleep(40)
mem1 = (PROBE.working_set_mb(), rss_mb("monitor-agent"))
mem1 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"))
check("memory stays bounded after the storms", mem1[0] < mem0[0] + 15 and mem1[1] < mem0[1] + 15, f"probe {mem0[0]:.1f}->{mem1[0]:.1f} MB, agent {mem0[1]:.1f}->{mem1[1]:.1f} MB")
ensure_healthy()
@@ -501,7 +501,7 @@ def scenario_backend_link():
check("steady state: ~2 chunks / 10 s (udp + tcp), no duplicates", 4 <= c1 - c0 <= 8, f"{c1 - c0} chunks in 30 s")
# the backend is stopped for a while (graceful): the agent keeps collecting, delivers after
stop_proc("monitor-backend", "TERM", 30)
stop_proc("moshiro-backend", "TERM", 30)
time.sleep(40)
start_backend()
check("backend down 40 s then up: everything converges", wait_until(healthy, 90) is not None)
@@ -525,16 +525,16 @@ def scenario_silent():
check("... and when the probe thaws it comes back", wait_until(healthy, 90) is not None)
# a frozen agent: the probe must drop the session and keep retrying; the backend must mark the agent offline
wsl("pkill -STOP -x monitor-agent")
wsl("pkill -STOP -x moshiro-agent")
seen_off = wait_until(lambda: not agent_online(), 150, 1)
check("frozen agent: the backend marks it offline", seen_off is not None, f"{seen_off and round(seen_off)}s" if seen_off else "still online after 150 s")
wsl("pkill -CONT -x monitor-agent")
wsl("pkill -CONT -x moshiro-agent")
check("... and when the agent thaws everything converges", wait_until(healthy, 90) is not None)
# a frozen backend: the agent must notice (WebSocket ping timeout) and reconnect once the backend thaws
wsl("pkill -STOP -x monitor-backend")
wsl("pkill -STOP -x moshiro-backend")
time.sleep(70)
wsl("pkill -CONT -x monitor-backend")
wsl("pkill -CONT -x moshiro-backend")
check("frozen backend (70 s): converges after it thaws", wait_until(healthy, 90) is not None)
time.sleep(30)
g, n = gaps(3)
@@ -546,7 +546,7 @@ def scenario_soak():
print("== soak: 8 minutes with random faults; memory, convergence and data continuity")
ensure_healthy()
time.sleep(20)
mem0 = (PROBE.working_set_mb(), rss_mb("monitor-agent"), rss_mb("monitor-backend"))
mem0 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"), rss_mb("moshiro-backend"))
import random
rnd = random.Random(7)
end = now() + 8 * 60
@@ -563,7 +563,7 @@ def scenario_soak():
time.sleep(rnd.uniform(5, 12))
clear_netem("eth0")
elif fault == "agent_kill":
wsl("pkill -9 -x monitor-agent; true")
wsl("pkill -9 -x moshiro-agent; true")
time.sleep(1)
start_agent()
elif fault == "probe_kill":
@@ -574,7 +574,7 @@ def scenario_soak():
clear_netem("eth0")
check(f"after {faults} random faults everything converges", ensure_healthy(90) is not None)
time.sleep(45)
mem1 = (PROBE.working_set_mb(), rss_mb("monitor-agent"), rss_mb("monitor-backend"))
mem1 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"), rss_mb("moshiro-backend"))
check("memory bounded", mem1[0] < mem0[0] + 15 and mem1[1] < mem0[1] + 20 and mem1[2] < mem0[2] + 40, f"probe {mem0[0]:.0f}->{mem1[0]:.0f}, agent {mem0[1]:.0f}->{mem1[1]:.0f}, backend {mem0[2]:.0f}->{mem1[2]:.0f} MB")
m, _ = metrics("30s")
check("the final minute is measured correctly", (m.get("loss.up") or 0) < 1 and (m.get("loss.down") or 0) < 1, f"{m.get('loss.up')} {m.get('loss.down')}")
@@ -601,7 +601,7 @@ def main():
except Exception as ex:
check(f"{name} ran to completion", False, repr(ex)[:300])
PROBE.stop("kill")
wsl("pkill -9 -x monitor-agent; pkill -9 -x monitor-backend; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true")
wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true")
failed = [r for r in RESULTS if not r[1]]
print(f"\n{len(RESULTS) - len(failed)}/{len(RESULTS)} checks passed")
for name, _, detail in failed:
+5 -5
View File
@@ -8,8 +8,8 @@ import shlex
import subprocess
import tempfile
SCRIPT = Path(__file__).resolve().parents[1] / "packaging/update/monitor-update.sh"
with tempfile.TemporaryDirectory(prefix="monitor-updater-test-") as directory:
SCRIPT = Path(__file__).resolve().parents[1] / "packaging/update/moshiro-update.sh"
with tempfile.TemporaryDirectory(prefix="moshiro-updater-test-") as directory:
root = Path(directory)
(root / "bin").mkdir()
(root / "data/update").mkdir(parents=True)
@@ -39,8 +39,8 @@ case "${0##*/}" in
esac
""")
tool.chmod(0o755)
env = dict(os.environ, PATH=str(root / "bin") + os.pathsep + os.environ["PATH"], MONITOR_UPDATE_CONF_DIR=str(root / "conf"), TEST_LOG=str(log), TEST_MODE=str(mode))
repo = "registry.example/monitor/monitor-agent"
env = dict(os.environ, PATH=str(root / "bin") + os.pathsep + os.environ["PATH"], MOSHIRO_UPDATE_CONF_DIR=str(root / "conf"), TEST_LOG=str(log), TEST_MODE=str(mode))
repo = "registry.example/moshiro/moshiro-agent"
digest = "sha256:" + "a" * 64
image_file = root / "agent.image"
state = root / "state"
@@ -48,7 +48,7 @@ esac
auth.write_text('{"auths":{}}')
def configure(engine="podman", scope="system", policy="manual", explicit=True):
values = dict(DATA_DIR=str(root / "data"), IMAGE_REPO=repo, UNIT="monitor-agent.service", PUBKEY=str(pub), ENV_FILE=str(image_file), STATE_DIR=str(state), POLICY=policy, AUTHFILE=str(auth))
values = dict(DATA_DIR=str(root / "data"), IMAGE_REPO=repo, UNIT="moshiro-agent.service", PUBKEY=str(pub), ENV_FILE=str(image_file), STATE_DIR=str(state), POLICY=policy, AUTHFILE=str(auth))
if explicit:
values.update(ENGINE=engine, SERVICE_SCOPE=scope)
(root / "conf/agent.conf").write_text("".join(k + "=" + shlex.quote(v) + "\n" for k, v in values.items()))