refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
justme committed 2026-10-07 02:03:25 +08:00
1 parent 27ac7fb787
commit 2e146d4da7
309 files changed
+1565 -1572

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()))