scripts: consolidate Agent deployment and remove Probe packages
Use Agent for native, container, systemd and OpenWrt installations and signed updates. Remove retired Probe deployment templates and smoke scripts.
This commit is contained in:
1 parent
ad5efc62ff
commit
016dbc2bbf
30 files changed
+93
-1203
No files matched your search
@@ -14,7 +14,7 @@ services:
|
||||
container_name: moshiro-agent
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
# Probe 要直连 Agent 的 7443(TCP+UDP)和 7444(TCP),用宿主网络,不做端口映射
|
||||
# 其他 Agent 要直连此节点 的 7443(TCP+UDP)和 7444(TCP),用宿主网络,不做端口映射
|
||||
network_mode: host
|
||||
volumes:
|
||||
- agent-config:/etc/moshiro
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# MoShiro Probe。Docker 和 rootless/rootful Podman 共用。
|
||||
#
|
||||
# 用法(面板「添加 Probe」会给出带好参数的 .env 内容):
|
||||
# mkdir moshiro-probe && cd moshiro-probe
|
||||
# 把面板给的 .env 内容存成 .env,再下载本文件为 compose.yaml
|
||||
# podman compose up -d # Docker 用 docker compose
|
||||
# 更新:podman compose pull && podman compose up -d (身份和配置在命名卷里,不会动)
|
||||
# 固定版本:在 .env 里写 MOSHIRO_VERSION=<版本号>;想要更严,写成 MOSHIRO_VERSION=<版本号>@sha256:<digest>。
|
||||
# macOS、Windows 的 Docker Desktop 没有宿主网络:面板给的 .env 里已经写了 MOSHIRO_NETWORK_MODE=bridge。
|
||||
# .env 里的 MOSHIRO_SERVER、MOSHIRO_JOIN(只在第一次启动用来注册,之后可以删掉)、MOSHIRO_GENESIS、MOSHIRO_CONTROLLERS、MOSHIRO_NAME 原样传进容器。
|
||||
# 说明见 docs/ops/deployment.md。
|
||||
services:
|
||||
probe:
|
||||
image: ${MOSHIRO_IMAGE:-registry.oeo.one/moshiro/moshiro-probe}:${MOSHIRO_VERSION:-latest}
|
||||
container_name: moshiro-probe
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
network_mode: ${MOSHIRO_NETWORK_MODE:-host}
|
||||
# .env 里的变量原样传进容器(不用带默认值的变量替换:有的 compose 提供者,比如 podman-compose,不替换环境变量里的默认值)
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
HOST_PROC: /host/proc
|
||||
HOST_SYS: /host/sys
|
||||
HOST_ROOT: /host/root
|
||||
volumes:
|
||||
- probe-config:/etc/moshiro
|
||||
- probe-status:/etc/moshiro/update-status:ro
|
||||
- /etc/os-release:/etc/os-release:ro
|
||||
# 主机资源占用(CPU、内存、磁盘、网卡流量、运行时长,只有汇总数字,没有型号和 IP):只读挂载宿主机的 /proc、/sys 和根目录;
|
||||
# 不想上报:在 probe.json 里写 reporting.host_stats=false,再删掉下面三行挂载。Docker Desktop 上看到的是它的虚拟机,不是 Windows 或 macOS 本身。
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host/root:ro
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
|
||||
security_opt:
|
||||
- label=disable
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
# Linux ICMP 使用 ping sockets;ping_group_range 需允许容器映射后的 GID。
|
||||
stop_grace_period: 20s
|
||||
|
||||
volumes:
|
||||
probe-config:
|
||||
name: moshiro-probe-config
|
||||
probe-status:
|
||||
name: moshiro-probe-status
|
||||
@@ -2,7 +2,7 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
|
||||
|
||||
ARG APP_VERSION=unknown
|
||||
LABEL org.opencontainers.image.title="MoShiro Agent" \
|
||||
org.opencontainers.image.description="MoShiro node agent for Linux" \
|
||||
org.opencontainers.image.description="MoShiro node for Linux: listens and dials (MOSHIRO_MODE=agent) or only dials (MOSHIRO_MODE=dial)" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.version="$APP_VERSION"
|
||||
|
||||
@@ -12,10 +12,8 @@ COPY --chown=1654:1654 app/fleet-enroll /usr/local/bin/fleet-enroll
|
||||
COPY --chown=1654:1654 config/ /etc/moshiro/
|
||||
COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint
|
||||
|
||||
ENV MOSHIRO_ROLE=agent
|
||||
ENV LD_LIBRARY_PATH=/usr/local/bin
|
||||
USER 1654:1654
|
||||
WORKDIR /etc/moshiro
|
||||
VOLUME ["/etc/moshiro"]
|
||||
ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"]
|
||||
CMD ["--config", "/etc/moshiro/agent.json"]
|
||||
@@ -52,7 +52,6 @@ COPY --chown=1654:1654 --from=build /out/enroll/fleet-enroll /usr/local/bin/flee
|
||||
COPY --chown=1654:1654 config/ /etc/moshiro/
|
||||
COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint
|
||||
|
||||
ENV MOSHIRO_ROLE=agent
|
||||
USER 1654:1654
|
||||
WORKDIR /etc/moshiro
|
||||
VOLUME ["/etc/moshiro"]
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
|
||||
|
||||
ARG APP_VERSION=unknown
|
||||
LABEL org.opencontainers.image.title="MoShiro Probe" \
|
||||
org.opencontainers.image.description="MoShiro network measurement probe for Linux" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.version="$APP_VERSION"
|
||||
|
||||
COPY --chown=1654:1654 app/moshiro-probe /usr/local/bin/moshiro-probe
|
||||
COPY --chown=1654:1654 app/lib*.so.* /usr/local/bin/
|
||||
COPY --chown=1654:1654 app/fleet-enroll /usr/local/bin/fleet-enroll
|
||||
COPY --chown=1654:1654 config/ /etc/moshiro/
|
||||
COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint
|
||||
|
||||
ENV MOSHIRO_ROLE=probe
|
||||
ENV LD_LIBRARY_PATH=/usr/local/bin
|
||||
USER 1654:1654
|
||||
WORKDIR /etc/moshiro
|
||||
VOLUME ["/etc/moshiro"]
|
||||
ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"]
|
||||
CMD ["--config", "/etc/moshiro/probe.json"]
|
||||
@@ -1,60 +0,0 @@
|
||||
# AotAnywhere cross-links both Alpine architectures on the native amd64 runner.
|
||||
ARG NATIVEAOT_BUILDER_IMAGE=localhost/moshiro-dotnet10-nativeaot-aotanywhere:ci
|
||||
FROM --platform=linux/amd64 ${NATIVEAOT_BUILDER_IMAGE} AS build
|
||||
|
||||
WORKDIR /src/backend
|
||||
COPY restore/backend/ ./
|
||||
ARG NODE_RID=linux-musl-x64
|
||||
ENV NUGET_PACKAGES=/root/.nuget/packages
|
||||
RUN dotnet restore probe/src/MoShiro.Probe -r "$NODE_RID" -p:PublishAot=true -p:UseAotAnywhere=true \
|
||||
-p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 \
|
||||
-p:InvariantGlobalization=true --artifacts-path /artifacts \
|
||||
&& dotnet restore fleet/tools/fleet-enroll -r "$NODE_RID" -p:PublishAot=true -p:UseAotAnywhere=true \
|
||||
-p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 -p:InvariantGlobalization=true \
|
||||
--artifacts-path /artifacts
|
||||
|
||||
COPY source/backend/ ./
|
||||
COPY source/packaging/native/msquic/ /src/packaging/native/msquic/
|
||||
# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效)
|
||||
ARG MOSHIRO_TAG=
|
||||
ARG MOSHIRO_COMMIT=
|
||||
ARG MOSHIRO_BUILT_AT=
|
||||
ENV MOSHIRO_TAG=$MOSHIRO_TAG MOSHIRO_COMMIT=$MOSHIRO_COMMIT MOSHIRO_BUILT_AT=$MOSHIRO_BUILT_AT
|
||||
RUN dotnet publish probe/src/MoShiro.Probe -m:3 -c Release -r "$NODE_RID" \
|
||||
--self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=true \
|
||||
-p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 \
|
||||
-p:InvariantGlobalization=true -p:UseAppHost=true -p:StripSymbols=true \
|
||||
-p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/probe \
|
||||
&& dotnet publish fleet/tools/fleet-enroll -m:3 -c Release -r "$NODE_RID" \
|
||||
--self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=true \
|
||||
-p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 -p:InvariantGlobalization=true \
|
||||
-p:UseAppHost=true -p:StripSymbols=true \
|
||||
-p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/enroll \
|
||||
&& rm -rf /artifacts
|
||||
RUN test -x /out/probe/moshiro-probe && test -x /out/enroll/fleet-enroll \
|
||||
&& test "$(stat -c%s /out/probe/moshiro-probe)" -gt 2000000 \
|
||||
&& test "$(stat -c%s /out/enroll/fleet-enroll)" -gt 2000000 \
|
||||
&& ! readelf -l /out/probe/moshiro-probe | grep -q INTERP \
|
||||
&& ! readelf -d /out/probe/moshiro-probe 2>&1 | grep -q NEEDED \
|
||||
&& ! readelf -l /out/enroll/fleet-enroll | grep -q INTERP \
|
||||
&& ! readelf -d /out/enroll/fleet-enroll 2>&1 | grep -q NEEDED
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
|
||||
|
||||
ARG APP_VERSION=main
|
||||
LABEL org.opencontainers.image.title="MoShiro Probe" \
|
||||
org.opencontainers.image.description="MoShiro network measurement probe for Linux" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.version="$APP_VERSION"
|
||||
|
||||
COPY --chown=1654:1654 --from=build /out/probe/moshiro-probe /usr/local/bin/moshiro-probe
|
||||
COPY --chown=1654:1654 --from=build /out/enroll/fleet-enroll /usr/local/bin/fleet-enroll
|
||||
COPY --chown=1654:1654 config/ /etc/moshiro/
|
||||
COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint
|
||||
|
||||
ENV MOSHIRO_ROLE=probe
|
||||
USER 1654:1654
|
||||
WORKDIR /etc/moshiro
|
||||
VOLUME ["/etc/moshiro"]
|
||||
ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"]
|
||||
CMD ["--config", "/etc/moshiro/probe.json"]
|
||||
@@ -1,24 +1,22 @@
|
||||
#!/bin/sh
|
||||
# 部署或更新容器服务(Agent、Probe;Backend 用 Quadlet,见 packaging/quadlet/moshiro-backend.container)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、
|
||||
# 部署或更新容器服务(Agent;Backend 用 Quadlet,见 packaging/quadlet/moshiro-backend.container)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、
|
||||
# 重新安装单元和更新器,然后重启节点;已有的身份和配置不会被覆盖。
|
||||
#
|
||||
# 首次部署 Agent:sudo sh deploy-node.sh agent --image-repo REPO --server https://panel --join jt_xxx --name hk-1
|
||||
# 首次部署 Probe:sudo sh deploy-node.sh probe --image-repo REPO --server https://panel --join jt_xxx --name home-1
|
||||
# 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置)
|
||||
# 旧版 Probe(probe.json 里没有 fleet 段):加上 --server --join --name 重新运行即可,旧文件备份为 probe.json.old
|
||||
#
|
||||
# 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。
|
||||
# rootless Podman 见 docs/ops/deployment.md。
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "usage: deploy-node.sh <agent|probe> [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2
|
||||
echo "usage: deploy-node.sh <agent> [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2
|
||||
echo " [--server URL --join TOKEN --name NAME] [--policy auto|manual] [--authfile F]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
ROLE="${1:-}"; shift || true
|
||||
case "$ROLE" in agent|probe) ;; *) usage ;; esac
|
||||
case "$ROLE" in agent) ;; *) usage ;; esac
|
||||
ENGINE=""; REPO=""; TAG=""; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -121,7 +119,7 @@ in_volume() {
|
||||
}
|
||||
in_volume 'mkdir -p /etc/moshiro/update' </dev/null
|
||||
|
||||
# 3. 配置:身份由节点自己生成(Agent 和 Probe 一样),注册之后面板只认这把公钥
|
||||
# 3. 配置:身份由节点自己生成(所有 Agent 一样),注册之后面板只认这把公钥
|
||||
ID_DIR="/etc/moshiro/$ROLE-id"
|
||||
if ! in_volume "test -f $ID_DIR/node.json" </dev/null; then
|
||||
[ -n "$SERVER" ] && [ -n "$JOIN" ] && [ -n "$NAME" ] || { echo "first deployment needs --server, --join and --name" >&2; exit 1; }
|
||||
@@ -139,18 +137,7 @@ if [ "$ROLE" = agent ]; then
|
||||
in_volume 'sed -i "s/\"bind\" *: *\"0\.0\.0\.0\"/\"bind\": \"::\"/" /etc/moshiro/agent.json' </dev/null
|
||||
echo "config: listen.bind 0.0.0.0 -> ::"
|
||||
fi
|
||||
elif [ "$ROLE" = probe ]; then
|
||||
# 旧格式(面板生成的 probe.json:没有 fleet 段)备份之后按新格式重写
|
||||
if in_volume 'test -f /etc/moshiro/probe.json' </dev/null && ! in_volume 'grep -q "\"fleet\"" /etc/moshiro/probe.json' </dev/null; then
|
||||
in_volume 'mv /etc/moshiro/probe.json /etc/moshiro/probe.json.old' </dev/null
|
||||
echo "config: old probe.json moved to probe.json.old"
|
||||
fi
|
||||
if ! in_volume 'test -f /etc/moshiro/probe.json' </dev/null; then
|
||||
[ -n "$SERVER" ] || { echo "probe.json is missing; pass --server to create it" >&2; exit 1; }
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro/probe-id"}}\n' "$SERVER" \
|
||||
| in_volume 'umask 077; cat > /etc/moshiro/probe.json'
|
||||
echo "config: created probe.json"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# 4. 单元和更新器(保持原来的更新策略)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/bin/sh
|
||||
# 容器入口(Agent 和 Probe 共用):第一次启动用 MOSHIRO_SERVER / MOSHIRO_JOIN(MOSHIRO_NAME 不写就用主机名)自己注册并写配置,
|
||||
# 容器入口(监听和拨号两种模式共用,镜像里只有 moshiro-agent):第一次启动用 MOSHIRO_SERVER / MOSHIRO_JOIN(MOSHIRO_NAME 不写就用主机名)自己注册并写配置,
|
||||
# 身份和配置都在命名卷 /etc/moshiro 里,之后重启(或换新镜像重建容器)直接启动,不需要再给参数。
|
||||
set -eu
|
||||
|
||||
role="${MOSHIRO_ROLE:?MOSHIRO_ROLE is set by the image}"
|
||||
# 所有节点默认同时监听和拨号,身份与配置统一保存。
|
||||
role=agent
|
||||
# Quadlet 单元和 compose 默认用宿主网络;Docker Desktop(没有宿主网络)的 .env 里写 MOSHIRO_NETWORK_SCOPE=container
|
||||
export MOSHIRO_NETWORK_SCOPE="${MOSHIRO_NETWORK_SCOPE:-host}"
|
||||
dir=/etc/moshiro
|
||||
@@ -39,11 +40,7 @@ if [ ! -f "$config" ]; then
|
||||
echo "$config is missing: set MOSHIRO_SERVER to create it" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$role" = agent ]; then
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"%s"},"listen":{"bind":"::"}}\n' "$MOSHIRO_SERVER" "$identity" > "$config"
|
||||
else
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"%s"}}\n' "$MOSHIRO_SERVER" "$identity" > "$config"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "/usr/local/bin/moshiro-$role" "$@"
|
||||
exec /usr/local/bin/moshiro-agent --config "$config" "$@"
|
||||
+12
-17
@@ -1,10 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Installs moshiro-agent or moshiro-probe from an extracted release directory (the directory this script lives in).
|
||||
# Installs moshiro-agent or moshiro-agent from an extracted release directory (the directory this script lives in).
|
||||
# POSIX sh: runs on Debian/Ubuntu/Alpine/OpenWrt (busybox).
|
||||
#
|
||||
# ./install.sh agent --server https://panel.example.com --join <join-token> --name hk-1
|
||||
# ./install.sh probe --server https://panel --join jt_xxx --name home-1
|
||||
# ./install.sh uninstall agent|probe [--purge]
|
||||
# ./install.sh uninstall agent [--purge]
|
||||
#
|
||||
# Binaries go to /usr/lib/moshiro (root-owned: the collector cannot rewrite its own executable);
|
||||
# config and identity go to /etc/moshiro (owned by the unprivileged `moshiro` user).
|
||||
@@ -80,7 +79,7 @@ write_config() {
|
||||
}
|
||||
JSON
|
||||
else
|
||||
cat >"$CONFIG/probe.json" <<JSON
|
||||
cat >"$CONFIG/agent.json" <<JSON
|
||||
{
|
||||
"fleet": { "server": "$2", "identity_dir": "$CONFIG" }
|
||||
}
|
||||
@@ -122,8 +121,7 @@ install_role() {
|
||||
if [ "$have_user" -eq 0 ]; then sed -i '/^User=/d;/^Group=/d' "/etc/systemd/system/moshiro-$role.service"; fi
|
||||
systemctl daemon-reload ;;
|
||||
procd)
|
||||
[ "$role" = probe ] || die "the agent is meant for servers; on OpenWrt only the probe is supported"
|
||||
put 0755 "$HERE/openwrt/moshiro-probe.init" /etc/init.d/moshiro-probe
|
||||
put 0755 "$HERE/openwrt/moshiro-agent.init" /etc/init.d/moshiro-agent
|
||||
mkdir -p /lib/upgrade/keep.d
|
||||
put 0644 "$HERE/openwrt/moshiro.keep" /lib/upgrade/keep.d/moshiro ;;
|
||||
*) echo "warning: no systemd or procd found: start with LD_LIBRARY_PATH=$PREFIX $PREFIX/moshiro-$role --config $CONFIG/$role.json under your own supervisor" >&2 ;;
|
||||
@@ -145,14 +143,14 @@ install_role() {
|
||||
if [ "$ready" -eq 1 ] && [ "$no_start" -eq 0 ]; then
|
||||
case "$init" in
|
||||
systemd) systemctl enable --now "moshiro-$role" ;;
|
||||
procd) /etc/init.d/moshiro-probe enable; /etc/init.d/moshiro-probe restart ;;
|
||||
procd) /etc/init.d/moshiro-agent enable; /etc/init.d/moshiro-agent restart ;;
|
||||
esac
|
||||
say "moshiro-$role is installed"
|
||||
else
|
||||
say "installed, not started yet. Next:"
|
||||
echo " $PREFIX/fleet-enroll --server <panel-url> --join <token> --name <name> --identity-dir $CONFIG (as the '$USER_NAME' user)"
|
||||
echo " write $CONFIG/$role.json (see README.md), then: systemctl enable --now moshiro-$role"
|
||||
[ "$role" = agent ] || echo " (OpenWrt: /etc/init.d/moshiro-probe enable && /etc/init.d/moshiro-probe start)"
|
||||
echo " (OpenWrt: /etc/init.d/moshiro-agent enable && /etc/init.d/moshiro-agent start)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -165,15 +163,13 @@ uninstall() {
|
||||
rm -f "/etc/systemd/system/moshiro-$role.service"
|
||||
systemctl daemon-reload ;;
|
||||
procd)
|
||||
/etc/init.d/moshiro-probe stop 2>/dev/null || true
|
||||
/etc/init.d/moshiro-probe disable 2>/dev/null || true
|
||||
rm -f /etc/init.d/moshiro-probe /lib/upgrade/keep.d/moshiro ;;
|
||||
/etc/init.d/moshiro-agent stop 2>/dev/null || true
|
||||
/etc/init.d/moshiro-agent disable 2>/dev/null || true
|
||||
rm -f /etc/init.d/moshiro-agent /lib/upgrade/keep.d/moshiro ;;
|
||||
esac
|
||||
rm -f "$PREFIX/moshiro-$role"
|
||||
rm -f "$PREFIX/fleet-enroll"
|
||||
other_role=agent
|
||||
[ "$role" = agent ] && other_role=probe
|
||||
[ -x "$PREFIX/moshiro-$other_role" ] || rm -f "$PREFIX"/lib*.so.*
|
||||
rm -f "$PREFIX"/lib*.so.*
|
||||
rmdir "$PREFIX" 2>/dev/null || true
|
||||
if [ "$purge" -eq 1 ]; then
|
||||
rm -rf "$CONFIG"
|
||||
@@ -185,12 +181,11 @@ uninstall() {
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
agent|probe) shift; install_role "$cmd" "$@" ;;
|
||||
agent) shift; install_role "$cmd" "$@" ;;
|
||||
uninstall)
|
||||
shift
|
||||
role="${1:-}"
|
||||
[ "$role" = agent ] || [ "$role" = probe ] || die "usage: install.sh uninstall agent|probe [--purge]"
|
||||
shift
|
||||
uninstall "$role" "$@" ;;
|
||||
*) die "usage: install.sh agent|probe [options] | uninstall agent|probe [--purge]" ;;
|
||||
*) die "usage: install.sh agent [options] | uninstall agent [--purge]" ;;
|
||||
esac
|
||||
@@ -1,6 +1,6 @@
|
||||
# MoShiro 节点一键安装(Windows,Agent 和 Probe 共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
|
||||
# MoShiro 节点一键安装(Windows,监听和拨号两种模式共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
|
||||
# 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行。
|
||||
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(agent 或 probe)、MOSHIRO_CODE(一次性的加入码)。
|
||||
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(默认 agent,监听并拨号)、MOSHIRO_CODE(一次性的加入码)。
|
||||
# 做的事:用 Docker Desktop 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取计算机名。
|
||||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_GENESIS / MOSHIRO_CONTROLLERS(命令里带着:信任根指纹和这个节点的属主 passkey 凭证 id,都是公开值)、
|
||||
# MOSHIRO_ALLOW_UNVERIFIED=1 加 MOSHIRO_IMAGE=镜像(面板还没有已签名的发布清单时,不验签、按这个镜像安装;默认拒绝)。
|
||||
@@ -31,7 +31,7 @@ function Test-MoShiroSignature([byte[]]$data, [byte[]]$derSignature, [string]$pe
|
||||
|
||||
function Install-MoShiro {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$role = $env:MOSHIRO_ROLE
|
||||
$role = if ($env:MOSHIRO_ROLE) { $env:MOSHIRO_ROLE } else { 'agent' }
|
||||
$server = $env:MOSHIRO_SERVER
|
||||
$code = $env:MOSHIRO_CODE
|
||||
$image = $env:MOSHIRO_IMAGE
|
||||
@@ -43,7 +43,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
|
||||
'@
|
||||
|
||||
# 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里
|
||||
if ($role -notin @('agent', 'probe')) { Write-Error 'MOSHIRO_ROLE must be agent or probe'; return }
|
||||
if ($role -notin @('agent')) { Write-Error 'MOSHIRO_ROLE must be agent'; return }
|
||||
if ($server -notmatch '^https?://[A-Za-z0-9._:/@~%+\[\]-]+$') { Write-Error 'MOSHIRO_SERVER must be the panel address (http:// or https://)'; return }
|
||||
if ($code -notmatch '^[A-Za-z0-9._~%+-]+$') { Write-Error 'MOSHIRO_CODE is required (copy the whole command from the panel)'; return }
|
||||
|
||||
@@ -83,7 +83,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
|
||||
}
|
||||
if ($null -eq $manifest) {
|
||||
if ($env:MOSHIRO_ALLOW_UNVERIFIED -ne '1' -or -not $image) {
|
||||
Write-Error "the panel has no signed release for $role yet, so the image cannot be verified. To install anyway set `$env:MOSHIRO_ALLOW_UNVERIFIED='1' and `$env:MOSHIRO_IMAGE"
|
||||
Write-Error "the panel has no signed release yet, so the image cannot be verified. To install anyway set `$env:MOSHIRO_ALLOW_UNVERIFIED='1' and `$env:MOSHIRO_IMAGE"
|
||||
return
|
||||
}
|
||||
Write-Host "warning: installing $image without signature verification"
|
||||
@@ -93,7 +93,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
|
||||
return
|
||||
}
|
||||
$release = [Text.Encoding]::UTF8.GetString($manifest) | ConvertFrom-Json
|
||||
if ($release.component -ne $role) { Write-Error "the signed release is for '$($release.component)', not '$role'"; return }
|
||||
if ($release.component -ne 'agent') { Write-Error "the signed release is for '$($release.component)', not 'agent'"; return }
|
||||
if ($release.digest -notmatch '^sha256:[0-9a-f]{64}$') { Write-Error 'the signed release has a malformed digest'; return }
|
||||
if ($release.image -notmatch '^[A-Za-z0-9._/:-]+$') { Write-Error 'the signed release has a malformed image name'; return }
|
||||
$image = "$($release.image)@$($release.digest)"
|
||||
@@ -110,9 +110,9 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
|
||||
$arguments = @('run', '-d', '--name', $container, '--restart', 'unless-stopped',
|
||||
'--read-only', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', '--cap-drop', 'all', '--security-opt', 'no-new-privileges',
|
||||
'-v', "${volume}:/etc/moshiro")
|
||||
# Docker Desktop 没有宿主网络:Agent 把端口发布出来(面板里的地址需要指向这台机器的公网地址)
|
||||
# Docker Desktop 没有宿主网络:监听的节点把端口发布出来(面板里的地址需要指向这台机器的公网地址)
|
||||
if ($role -eq 'agent') { $arguments += @('-p', '7443:7443/tcp', '-p', '7443:7443/udp', '-p', '7444:7444/tcp') }
|
||||
$arguments += @('-e', "MOSHIRO_SERVER=$server", '-e', "MOSHIRO_JOIN=$code", '-e', "MOSHIRO_NAME=$name")
|
||||
$arguments += @('-e', "MOSHIRO_SERVER=$server", '-e', "MOSHIRO_JOIN=$code", '-e', "MOSHIRO_NAME=$name", '-e', "MOSHIRO_MODE=$role")
|
||||
# 每个节点都必须有属主(passkey):命令里的信任根指纹和属主缺一个就不装(面板的命令总是带着;只有没有信任根的老面板才需要 MOSHIRO_ALLOW_NO_OWNER=1)
|
||||
if ((-not $env:MOSHIRO_GENESIS -or -not $env:MOSHIRO_CONTROLLERS) -and $env:MOSHIRO_ALLOW_NO_OWNER -ne '1') {
|
||||
Write-Error 'this node has no owner: MOSHIRO_GENESIS and MOSHIRO_CONTROLLERS are required (copy the complete command from the panel; set up the trust root first)'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
# MoShiro 节点一键安装(Agent 和 Probe 共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
|
||||
# MoShiro 节点一键安装(监听和拨号两种模式共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
|
||||
# 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行:
|
||||
# f=$(mktemp) && curl -fsSL 'https://panel/install' -o "$f" && echo "<sha256> $f" | sha256sum -c - && MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=… sh "$f"
|
||||
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(agent 或 probe)、MOSHIRO_CODE(一次性的加入码)。
|
||||
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(默认 agent,监听并拨号)、MOSHIRO_CODE(一次性的加入码)。
|
||||
# 做的事:用 Docker 或 Podman 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取主机名。
|
||||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_ENGINE=docker|podman、MOSHIRO_DRY_RUN=1(只打印命令)、
|
||||
# MOSHIRO_GENESIS / MOSHIRO_CONTROLLERS(命令里带着:你的信任根指纹和这个节点的属主 passkey 凭证 id,节点在注册时验证面板的操作员集合并把它们固定在本地;都是公开值)、
|
||||
@@ -11,7 +11,7 @@
|
||||
set -eu
|
||||
|
||||
SERVER="${MOSHIRO_SERVER:-}"
|
||||
ROLE="${MOSHIRO_ROLE:-}"
|
||||
ROLE="${MOSHIRO_ROLE:-agent}"
|
||||
CODE="${MOSHIRO_CODE:-}"
|
||||
IMAGE="${MOSHIRO_IMAGE:-}"
|
||||
RELEASE_KEY='-----BEGIN PUBLIC KEY-----
|
||||
@@ -23,7 +23,7 @@ say() { printf '%s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里
|
||||
case "$ROLE" in agent|probe) ;; *) die "MOSHIRO_ROLE must be agent or probe" ;; esac
|
||||
case "$ROLE" in agent) ;; *) die "MOSHIRO_ROLE must be agent" ;; esac
|
||||
case "$SERVER" in http://*|https://*) ;; *) die "MOSHIRO_SERVER must be the panel address (http:// or https://)" ;; esac
|
||||
case "$SERVER$CODE" in *[!A-Za-z0-9._:/@~%+\[\]-]*|'') die "MOSHIRO_SERVER or MOSHIRO_CODE has unexpected characters" ;; esac
|
||||
[ -n "$CODE" ] || die "MOSHIRO_CODE is required (copy the whole command from the panel)"
|
||||
@@ -70,7 +70,7 @@ resolve_image() {
|
||||
curl -fsS "$SERVER/install/manifest?code=$CODE" -o "$tmp/manifest" 2>/dev/null </dev/null || have_manifest=0
|
||||
curl -fsS "$SERVER/install/manifest.sig?code=$CODE" -o "$tmp/sig.b64" 2>/dev/null </dev/null || have_manifest=0
|
||||
if [ "$have_manifest" -eq 0 ]; then
|
||||
[ "${MOSHIRO_ALLOW_UNVERIFIED:-}" = 1 ] && [ -n "$IMAGE" ] || die "the panel has no signed release for $ROLE yet, so the image cannot be verified. To install anyway set MOSHIRO_ALLOW_UNVERIFIED=1 and MOSHIRO_IMAGE=<image>"
|
||||
[ "${MOSHIRO_ALLOW_UNVERIFIED:-}" = 1 ] && [ -n "$IMAGE" ] || die "the panel has no signed release yet, so the image cannot be verified. To install anyway set MOSHIRO_ALLOW_UNVERIFIED=1 and MOSHIRO_IMAGE=<image>"
|
||||
say "warning: installing $IMAGE without signature verification"
|
||||
return 0
|
||||
fi
|
||||
@@ -80,7 +80,7 @@ resolve_image() {
|
||||
openssl dgst -sha256 -verify "$tmp/key.pem" -signature "$tmp/sig.bin" "$tmp/manifest" >/dev/null 2>&1 \
|
||||
|| die "the release signature does not verify against the built-in release key; refusing to install"
|
||||
field() { sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" "$tmp/manifest" | head -n 1; }
|
||||
[ "$(field component)" = "$ROLE" ] || die "the signed release is for '$(field component)', not '$ROLE'"
|
||||
[ "$(field component)" = agent ] || die "the signed release is for '$(field component)', not 'agent'"
|
||||
repo="$(field image)"
|
||||
digest="$(field digest)"
|
||||
case "$digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) die "the signed release has a malformed digest" ;; esac
|
||||
@@ -97,18 +97,16 @@ set -- run -d --name "$container" --restart "$restart" \
|
||||
-v "$volume:/etc/moshiro"
|
||||
[ "$engine" = podman ] && set -- "$@" --security-opt label=disable
|
||||
if [ "$linux" -eq 1 ]; then
|
||||
# 宿主网络:Agent 对外暴露端口,也靠它识别本机的公网 IPv4 和 IPv6 地址
|
||||
# 宿主网络:监听的节点对外暴露端口,两种模式都靠它识别本机的公网 IPv4 和 IPv6 地址
|
||||
set -- "$@" --network host
|
||||
if [ "$ROLE" = agent ]; then
|
||||
# 主机资源占用(只读),管理端的 Agent 详情页显示 CPU、内存、磁盘、网络
|
||||
# 主机资源占用(只读),管理端的节点详情页显示 CPU、内存、磁盘、网络
|
||||
set -- "$@" -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro \
|
||||
-e MOSHIRO_NETWORK_SCOPE=host -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root
|
||||
fi
|
||||
elif [ "$ROLE" = agent ]; then
|
||||
# macOS 上的 Docker 没有宿主网络:把 Agent 的端口发布出来(面板里的地址需要指向这台机器的公网地址)
|
||||
# macOS 上的 Docker 没有宿主网络:把监听节点的端口发布出来(面板里的地址需要指向这台机器的公网地址)
|
||||
set -- "$@" -p 7443:7443/tcp -p 7443:7443/udp -p 7444:7444/tcp
|
||||
fi
|
||||
set -- "$@" -e "MOSHIRO_SERVER=$SERVER" -e "MOSHIRO_JOIN=$CODE" -e "MOSHIRO_NAME=$name"
|
||||
set -- "$@" -e "MOSHIRO_SERVER=$SERVER" -e "MOSHIRO_JOIN=$CODE" -e "MOSHIRO_NAME=$name" -e "MOSHIRO_MODE=$ROLE"
|
||||
# 每个节点都必须有属主(passkey):命令里的信任根指纹和属主缺一个就不装(面板的命令总是带着;只有没有信任根的老面板才需要 MOSHIRO_ALLOW_NO_OWNER=1)
|
||||
if [ -z "${MOSHIRO_GENESIS:-}" ] || [ -z "${MOSHIRO_CONTROLLERS:-}" ]; then
|
||||
[ "${MOSHIRO_ALLOW_NO_OWNER:-}" = 1 ] || die "this node has no owner: MOSHIRO_GENESIS and MOSHIRO_CONTROLLERS are required (copy the complete command from the panel; set up the trust root first)"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Native OpenWrt Probe installer/updater. No container engine or .NET runtime.
|
||||
# Native OpenWrt installer/updater for an Agent. No container engine or .NET runtime.
|
||||
# Bootstrap: verify this script's published hash before running `sh install.sh install`.
|
||||
# Trust/configuration live outside the collector's writable identity directory.
|
||||
set -eu
|
||||
@@ -9,7 +9,7 @@ ROOT=${MOSHIRO_NATIVE_ROOT:-/usr/lib/moshiro-native}
|
||||
CONF=${MOSHIRO_NATIVE_CONF:-/etc/moshiro-native.conf}
|
||||
STATE=${MOSHIRO_NATIVE_STATE:-/etc/moshiro-native-state}
|
||||
STATUS=${MOSHIRO_NATIVE_STATUS:-/var/run/moshiro-update}
|
||||
RUNTIME=${MOSHIRO_NATIVE_RUNTIME:-/var/run/moshiro-probe}
|
||||
RUNTIME=${MOSHIRO_NATIVE_RUNTIME:-/var/run/moshiro-agent}
|
||||
ACTION=${1:-install}
|
||||
FEED=https://git.oeo.one/api/packages/justme/generic/moshiro-openwrt
|
||||
CHANNEL=${MOSHIRO_CHANNEL:-release}
|
||||
@@ -26,7 +26,7 @@ die() { echo "moshiro-openwrt: $*" >&2; exit 1; }
|
||||
log() { echo "moshiro-openwrt: $*"; }
|
||||
ctl() {
|
||||
if [ "${MOSHIRO_NATIVE_TEST:-}" = 1 ]; then "$MOSHIRO_NATIVE_SERVICE" "$@"
|
||||
else /etc/init.d/moshiro-probe "$@"; fi
|
||||
else /etc/init.d/moshiro-agent "$@"; fi
|
||||
}
|
||||
field() { jsonfilter -i "$WORK/manifest.json" -e "@.$1"; }
|
||||
download() {
|
||||
@@ -121,7 +121,7 @@ verify() {
|
||||
download "$FEED/$CHANNEL/manifest-$RID.sig" 1024 "$WORK/manifest.sig" || return 1
|
||||
base64 -d "$WORK/manifest.sig" > "$WORK/signature" 2>/dev/null || return 1
|
||||
openssl dgst -sha256 -verify "$STATE/release-key.pem" -signature "$WORK/signature" "$WORK/manifest.json" >/dev/null 2>&1 || return 1
|
||||
[ "$(field schema)" = 1 ] && [ "$(field component)" = probe ] \
|
||||
[ "$(field schema)" = 1 ] && [ "$(field component)" = agent ] \
|
||||
&& [ "$(field channel)" = "$CHANNEL" ] && [ "$(field rid)" = "$RID" ] || return 1
|
||||
HASH=$(field sha256); SIZE=$(field size); SEQ=$(field seq); VERSION=$(field version); PACKAGE=$(field package)
|
||||
case "$HASH" in ''|*[!0-9a-f]*) return 1 ;; esac
|
||||
@@ -130,7 +130,7 @@ verify() {
|
||||
[ -n "$SEQ" ] && [ "${#SEQ}" -le 15 ] && [ -n "$SIZE" ] && [ "${#SIZE}" -le 9 ] && [ "$SIZE" -gt 0 ] && [ "$SIZE" -le "$MAX_BYTES" ] || return 1
|
||||
case "$VERSION" in ''|*[!A-Za-z0-9.+_-]*) return 1 ;; esac
|
||||
[ "${#VERSION}" -le 100 ] && printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$' || return 1
|
||||
[ "$PACKAGE" = "probe-$RID-$HASH.tar.gz" ] || return 1
|
||||
[ "$PACKAGE" = "agent-$RID-$HASH.tar.gz" ] || return 1
|
||||
}
|
||||
|
||||
switch_to() {
|
||||
@@ -168,7 +168,7 @@ stage() {
|
||||
[ "$(sort -u "$WORK/members" | wc -l)" -eq "$(wc -l < "$WORK/members")" ] || return 1
|
||||
while IFS= read -r member; do
|
||||
case "$member" in
|
||||
moshiro-probe|fleet-enroll|openwrt.sh|probe.init|updater.init) ;;
|
||||
moshiro-agent|fleet-enroll|openwrt.sh|agent.init|updater.init) ;;
|
||||
lib*.so.*) case "$member" in *[!A-Za-z0-9._+-]*) return 1 ;; esac ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
@@ -185,7 +185,7 @@ stage() {
|
||||
if ! tar -xzf "$WORK/package.tar.gz" -C "$STAGED"; then
|
||||
return 1
|
||||
fi
|
||||
for required in moshiro-probe fleet-enroll openwrt.sh probe.init updater.init; do
|
||||
for required in moshiro-agent fleet-enroll openwrt.sh agent.init updater.init; do
|
||||
[ -s "$STAGED/$required" ] || return 1
|
||||
chmod 755 "$STAGED/$required" || return 1
|
||||
done
|
||||
@@ -206,7 +206,7 @@ healthy() {
|
||||
version=$(jsonfilter -i "$health" -e '@.version' 2>/dev/null || true)
|
||||
pid=$(jsonfilter -i "$health" -e '@.pid' 2>/dev/null || true)
|
||||
at=$(jsonfilter -i "$health" -e '@.at' 2>/dev/null || echo 0)
|
||||
active=$(ubus call service list '{"name":"moshiro-probe"}' 2>/dev/null | jsonfilter -e '@["moshiro-probe"].instances.*.pid' 2>/dev/null || true)
|
||||
active=$(ubus call service list '{"name":"moshiro-agent"}' 2>/dev/null | jsonfilter -e '@["moshiro-agent"].instances.*.pid' 2>/dev/null || true)
|
||||
case "$pid:$at" in ''|*[!0-9:]*) ;; *)
|
||||
now=$(date +%s)
|
||||
if [ "$version" = "$VERSION" ] && [ -n "$pid" ] && [ "$pid" = "$active" ] && [ "$at" -ge "$started" ] && [ $((now - at)) -le 30 ]; then
|
||||
@@ -280,9 +280,9 @@ install() {
|
||||
: "${MOSHIRO_SERVER:?panel URL required}" "${MOSHIRO_CODE:?join token required}" "${MOSHIRO_GENESIS:?trust root required}" "${MOSHIRO_CONTROLLERS:?owner required}"
|
||||
case "$MOSHIRO_SERVER" in https://*) ;; *) die "panel must use HTTPS" ;; esac
|
||||
case "$MOSHIRO_SERVER" in *[!A-Za-z0-9:/._-]*) die "unsupported panel URL characters" ;; esac
|
||||
[ ! -e /etc/moshiro/node.json ] && [ ! -e /etc/moshiro/probe.json ] || die "existing native identity/config found; migrate explicitly instead of overwriting it"
|
||||
[ ! -e /etc/moshiro/node.json ] && [ ! -e /etc/moshiro/agent.json ] || die "existing native identity/config found; migrate explicitly instead of overwriting it"
|
||||
else
|
||||
[ -f /etc/moshiro/node.json ] && [ -f /etc/moshiro/trust.json ] && [ -f /etc/moshiro/probe.json ] || die "saved identity/config incomplete; restore a backup before reinstalling"
|
||||
[ -f /etc/moshiro/node.json ] && [ -f /etc/moshiro/trust.json ] && [ -f /etc/moshiro/agent.json ] || die "saved identity/config incomplete; restore a backup before reinstalling"
|
||||
fi
|
||||
acquire || die "another installation is running"
|
||||
if [ ! -f "$STATE/release-key.pem" ]; then
|
||||
@@ -301,7 +301,7 @@ KEY
|
||||
uid=900
|
||||
while awk -F: -v uid="$uid" '$3 == uid {found=1} END {exit !found}' /etc/passwd /etc/group; do uid=$((uid + 1)); done
|
||||
echo "moshiro:x:$uid:" >> /etc/group
|
||||
echo "moshiro:x:$uid:$uid:MoShiro:/var/run/moshiro-probe:/bin/false" >> /etc/passwd
|
||||
echo "moshiro:x:$uid:$uid:MoShiro:/var/run/moshiro-agent:/bin/false" >> /etc/passwd
|
||||
}
|
||||
mkdir -p /etc/moshiro "$RUNTIME"
|
||||
chmod 750 /etc/moshiro "$RUNTIME"
|
||||
@@ -310,7 +310,7 @@ KEY
|
||||
LD_LIBRARY_PATH="$ROOT/versions/$HASH" "$ROOT/versions/$HASH/fleet-enroll" \
|
||||
--server "$MOSHIRO_SERVER" --join "$MOSHIRO_CODE" --name "${MOSHIRO_NAME:-$(uname -n)}" \
|
||||
--identity-dir /etc/moshiro --genesis "$MOSHIRO_GENESIS" --controllers "$MOSHIRO_CONTROLLERS"
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro"},"limits":{"max_test_mbps":20,"max_total_pps":2000}}\n' "$MOSHIRO_SERVER" > /etc/moshiro/probe.json
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro"},"limits":{"max_test_mbps":20,"max_total_pps":2000}}\n' "$MOSHIRO_SERVER" > /etc/moshiro/agent.json
|
||||
fi
|
||||
chown -R moshiro:moshiro /etc/moshiro "$RUNTIME"
|
||||
chmod 600 /etc/moshiro/node.key /etc/moshiro/tls.pfx
|
||||
@@ -321,9 +321,9 @@ KEY
|
||||
switch_to "versions/$HASH"
|
||||
mkdir -p /usr/libexec
|
||||
ln -sf "$ROOT/current/openwrt.sh" /usr/libexec/moshiro-openwrt
|
||||
cp "$ROOT/current/probe.init" /etc/init.d/moshiro-probe
|
||||
cp "$ROOT/current/updater.init" /etc/init.d/moshiro-probe-update
|
||||
chmod 755 /etc/init.d/moshiro-probe /etc/init.d/moshiro-probe-update
|
||||
cp "$ROOT/current/agent.init" /etc/init.d/moshiro-agent
|
||||
cp "$ROOT/current/updater.init" /etc/init.d/moshiro-agent-update
|
||||
chmod 755 /etc/init.d/moshiro-agent /etc/init.d/moshiro-agent-update
|
||||
mkdir -p /lib/upgrade/keep.d
|
||||
printf '/etc/moshiro/\n%s\n%s/\n' "$CONF" "$STATE" > /lib/upgrade/keep.d/moshiro
|
||||
printf '%s\n' "$SEQ" > "$STATE/$CHANNEL.seq"
|
||||
@@ -336,8 +336,8 @@ KEY
|
||||
die "installed but not healthy; inspect logread -e moshiro, then rerun install (identity is preserved)"
|
||||
fi
|
||||
status up_to_date
|
||||
/etc/init.d/moshiro-probe-update enable
|
||||
/etc/init.d/moshiro-probe-update restart
|
||||
/etc/init.d/moshiro-agent-update enable
|
||||
/etc/init.d/moshiro-agent-update restart
|
||||
log "installed $VERSION ($CHANNEL/$RID); automatic signed updates enabled"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# procd service for moshiro-probe (OpenWrt). Installed as /etc/init.d/moshiro-probe.
|
||||
# procd service for moshiro-agent (OpenWrt). Installed as /etc/init.d/moshiro-agent.
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/lib/moshiro/moshiro-probe
|
||||
CONF=/etc/moshiro/probe.json
|
||||
PROG=/usr/lib/moshiro/moshiro-agent
|
||||
CONF=/etc/moshiro/agent.json
|
||||
|
||||
start_service() {
|
||||
[ -f "$CONF" ] || {
|
||||
logger -t moshiro-probe "$CONF not found: enroll first (install.sh probe --server URL --join TOKEN --name NAME)"
|
||||
logger -t moshiro-agent "$CONF not found: enroll first (install.sh agent --server URL --join TOKEN --name NAME)"
|
||||
return 1
|
||||
}
|
||||
procd_open_instance
|
||||
@@ -4,12 +4,12 @@ STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
start_service() {
|
||||
[ -f /etc/moshiro/probe.json ] || return 1
|
||||
mkdir -p /var/run/moshiro-probe
|
||||
chown moshiro:moshiro /var/run/moshiro-probe
|
||||
chmod 750 /var/run/moshiro-probe
|
||||
[ -f /etc/moshiro/agent.json ] || return 1
|
||||
mkdir -p /var/run/moshiro-agent
|
||||
chown moshiro:moshiro /var/run/moshiro-agent
|
||||
chmod 750 /var/run/moshiro-agent
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/lib/moshiro-native/current/moshiro-probe --config /etc/moshiro/probe.json
|
||||
procd_set_param command /usr/lib/moshiro-native/current/moshiro-agent --config /etc/moshiro/agent.json
|
||||
procd_set_param env LD_LIBRARY_PATH=/usr/lib/moshiro-native/current MOSHIRO_UPDATE=openwrt
|
||||
procd_set_param user moshiro
|
||||
procd_set_param group moshiro
|
||||
@@ -1,4 +1,4 @@
|
||||
# MoShiro Backend(面板)的 Podman Quadlet 单元(Podman 4.6 及以上),更新方式和 Agent、Probe 完全一样:
|
||||
# MoShiro Backend(面板)的 Podman Quadlet 单元(Podman 4.6 及以上),更新方式和 Agent 完全一样:
|
||||
# systemd 每次启动这个单元时 Pull=always 先从镜像仓库拉最新的镜像,Backend 在管理端确认更新之后有序退出(先把数据库备份到数据卷的 backups/),
|
||||
# Restart=always 把它拉起来,退出再被拉起就是更新(Backend 自己不下载、不替换任何东西)。
|
||||
# 不会自动重启:Backend 指挥所有节点,更新要管理员在管理端点「更新并重启」。
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# MoShiro Probe 的 Podman Quadlet 单元(Podman 4.6 及以上)。节点的更新也靠它:systemd 每次启动这个单元时 Pull=always 先从镜像仓库拉最新的镜像,
|
||||
# 所以节点退出再被拉起就是更新(节点自己不下载、不替换任何东西)。默认自动更新:面板发现新版本后,节点运行满 10 分钟就自己有序退出;
|
||||
# 想改成在管理端点「更新」才更新,在 env 文件里写 MOSHIRO_UPDATE_POLICY=manual。
|
||||
#
|
||||
# 安装(rootless,普通用户,不用 sudo):
|
||||
# mkdir -p ~/.config/containers/systemd ~/.config/moshiro
|
||||
# 把面板「添加 Probe」给的 .env 内容存成 ~/.config/moshiro/probe.env,chmod 600
|
||||
# 把本文件放进 ~/.config/containers/systemd/
|
||||
# systemctl --user daemon-reload && systemctl --user start moshiro-probe # Quadlet 会自动生成服务,不用 enable;[Install] 让它开机自启
|
||||
# loginctl enable-linger $USER # 退出登录后继续运行(需要管理员执行一次)
|
||||
# rootful:本文件放进 /etc/containers/systemd/,.env 内容存成 /etc/moshiro/probe.env,把下面的 EnvironmentFile 改成这个路径、
|
||||
# WantedBy 改成 multi-user.target,用 systemctl(不加 --user)。
|
||||
# 开发通道:把 Image 的 moshiro 换成 moshiro-dev。固定版本:把 :latest 换成 :<版本号>(固定之后更新不会换版本)。
|
||||
# 想要完全自动、不用在管理端点:另外启用 podman-auto-update.timer,并在 [Container] 里加 AutoUpdate=registry(Pull=always 也保留)。
|
||||
# 说明见 docs/ops/deployment.md。
|
||||
[Unit]
|
||||
Description=MoShiro Probe
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Container]
|
||||
Image=registry.oeo.one/moshiro/moshiro-probe:latest
|
||||
ContainerName=moshiro-probe
|
||||
Pull=always
|
||||
Network=host
|
||||
RunInit=true
|
||||
ReadOnly=true
|
||||
NoNewPrivileges=true
|
||||
DropCapability=ALL
|
||||
SecurityLabelDisable=true
|
||||
Tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777
|
||||
StopTimeout=20
|
||||
Volume=moshiro-probe-config:/etc/moshiro
|
||||
Volume=/etc/os-release:/etc/os-release:ro
|
||||
# 主机资源占用(只有汇总数字):只读挂载宿主机的 /proc、/sys 和根目录;不想上报就删掉这三行和下面三个 HOST_ 变量
|
||||
Volume=/proc:/host/proc:ro
|
||||
Volume=/sys:/host/sys:ro
|
||||
Volume=/:/host/root:ro
|
||||
Environment=HOST_PROC=/host/proc
|
||||
Environment=HOST_SYS=/host/sys
|
||||
Environment=HOST_ROOT=/host/root
|
||||
Environment=MOSHIRO_NETWORK_SCOPE=host
|
||||
Environment=LD_LIBRARY_PATH=/usr/local/bin
|
||||
# 告诉节点:退出之后外面会重新拉取,所以「更新」就是退出(没有这一行,节点不报告更新状态)
|
||||
Environment=MOSHIRO_UPDATE=restart
|
||||
EnvironmentFile=%h/.config/moshiro/probe.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# 第一次拉取镜像可能要一会儿
|
||||
TimeoutStartSec=600
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=MoShiro Probe (Podman container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/moshiro-update/probe.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f moshiro-probe
|
||||
ExecStart=/usr/bin/podman run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 moshiro-probe
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=MoShiro Probe (Docker container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/moshiro-update/probe.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/docker rm -f moshiro-probe
|
||||
ExecStart=/usr/bin/docker run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/docker stop -t 20 moshiro-probe
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=MoShiro Probe (rootless Podman container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=%h/.config/moshiro/probe.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f moshiro-probe
|
||||
ExecStart=/usr/bin/podman run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 moshiro-probe
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,44 +0,0 @@
|
||||
[Unit]
|
||||
Description=MoShiro probe (network line quality measurement origin)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
User=moshiro
|
||||
Group=moshiro
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/moshiro
|
||||
ExecStart=/usr/lib/moshiro/moshiro-probe --config /etc/moshiro/probe.json
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
# The probe is a pure collector: it never starts processes and writes no measurement data.
|
||||
# The only file the probe writes is agents.json (the latest agent list) next to probe.json.
|
||||
ReadWritePaths=/etc/moshiro
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
LockPersonality=yes
|
||||
# NativeAOT does not JIT, so writable+executable memory is never needed.
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallArchitectures=native
|
||||
# No capabilities needed: the probe only dials out.
|
||||
CapabilityBoundingSet=
|
||||
|
||||
MemoryMax=64M
|
||||
TasksMax=64
|
||||
LimitNOFILE=4096
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -5,7 +5,7 @@
|
||||
set -eu
|
||||
|
||||
COMPONENT="${1:-}"; shift || true
|
||||
case "$COMPONENT" in agent|probe) ;; *) echo "usage: install-updater.sh <agent|probe> [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--channel dev|release] [--health-url URL] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac
|
||||
case "$COMPONENT" in agent) ;; *) echo "usage: install-updater.sh <agent> [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--channel dev|release] [--health-url URL] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac
|
||||
DATA_DIR=""; IMAGE_REPO=""; UNIT=""; POLICY=manual; AUTHFILE=""; ENGINE=podman; SERVICE_SCOPE=system; ENABLE=true; DATA_VOLUME=""; STATUS_VOLUME=""; HELPER_IMAGE=""
|
||||
CHANNEL=""; HEALTH_URL=""
|
||||
while [ $# -gt 0 ]; do
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/bin/sh
|
||||
# MoShiro 容器节点(Agent、Probe)的宿主机更新器。用法:moshiro-update <check|apply> <组件> 组件 = agent 或 probe
|
||||
# MoShiro 容器节点(Agent)的宿主机更新器。用法:moshiro-update <check|apply> <组件> 组件 = agent
|
||||
#
|
||||
# 信任模型(防篡改、防中间人):
|
||||
# - 只接受用发布私钥签名的清单(ECDSA P-256 / SHA-256,对清单文件的原始字节签名);公钥放在本机,面板和 Registry 都改不了它。
|
||||
# - 清单里写的是镜像的 digest:拉取时用 repo@sha256:…,内容寻址,Registry 或链路上的人换不出另一个镜像。
|
||||
# - 镜像仓库、组件、服务名都来自本机的配置(/etc/moshiro-update/<组件>.conf),清单里写什么都不会改变拉哪个仓库。
|
||||
# - 清单的 seq 必须比已安装的大(防止旧清单被重放回滚)。
|
||||
# - 业务进程(Agent、Probe)只能往数据目录里写「清单」和「请求」文件,更新由这个宿主机脚本完成,业务进程不执行任何命令。
|
||||
# - 业务进程(Agent)只能往数据目录里写「清单」和「请求」文件,更新由这个宿主机脚本完成,业务进程不执行任何命令。
|
||||
# Shell snippets below execute inside helper containers.
|
||||
# shellcheck disable=SC2016
|
||||
set -eu
|
||||
@@ -14,8 +14,8 @@ umask 077
|
||||
|
||||
CMD="${1:-}"
|
||||
COMPONENT="${2:-}"
|
||||
case "$CMD" in check|apply) ;; *) echo "usage: moshiro-update <check|apply> <agent|probe>" >&2; exit 2 ;; esac
|
||||
case "$COMPONENT" in agent|probe) ;; *) echo "component must be agent or probe" >&2; exit 2 ;; esac
|
||||
case "$CMD" in check|apply) ;; *) echo "usage: moshiro-update <check|apply> <agent>" >&2; exit 2 ;; esac
|
||||
case "$COMPONENT" in agent) ;; *) echo "component must be agent" >&2; exit 2 ;; esac
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
DEFAULT_CONF_DIR=/etc/moshiro-update
|
||||
@@ -55,7 +55,7 @@ pull_image() {
|
||||
"$ENGINE" pull --quiet "$1"
|
||||
fi
|
||||
}
|
||||
# 业务进程(容器里的 Agent、Probe)可写的目录只有 update/:它在里面放 offer.manifest、offer.sig、apply.request。
|
||||
# 业务进程(容器里的 Agent)可写的目录只有 update/:它在里面放 offer.manifest、offer.sig、apply.request。
|
||||
# 更新器在容器外运行:只读取普通文件(不跟符号链接)、读取之后先复制到自己的临时目录再验证和使用(防止验证之后被换掉),
|
||||
# 状态目录在 rootless 容器里另行只读挂载;更新器配置和已安装版本状态不挂入容器。
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
@@ -139,7 +139,7 @@ fetch_mailbox() {
|
||||
|
||||
now() { date +%s; }
|
||||
|
||||
# 状态文件:更新器写,Agent / Probe 从只读挂载中读取并随心跳上报。
|
||||
# 状态文件:更新器写,Agent 从只读挂载中读取并随心跳上报。
|
||||
write_status() { # state available_json error
|
||||
tmp="$WORK/status.json"
|
||||
printf '{"state":"%s","current":"%s","available":%s,"error":"%s","at":%s}\n' \
|
||||
|
||||
@@ -108,7 +108,7 @@ resolve() { # 签名私钥 组件 镜像仓库
|
||||
}
|
||||
expect "first install: signed manifest pins the digest" "$REPO@$DIGEST" "$(resolve "$T/b.pem" agent "$REPO")"
|
||||
expect "first install: wrong signing key is refused" "REFUSED" "$(resolve "$T/c.pem" agent "$REPO")"
|
||||
expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" probe "$REPO")"
|
||||
expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" backend "$REPO")"
|
||||
expect "first install: manifest naming another repository is refused" "REFUSED" "$(resolve "$T/b.pem" agent "registry.evil/moshiro-dev/moshiro-agent")"
|
||||
|
||||
# --- 第一次部署:正在跑的镜像就是清单里的这个 digest → 记成已安装(不当作有更新)
|
||||
|
||||
+8
-6
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# 打包:scripts/package.sh <agent|probe> <rid> → backend/artifacts/packages/moshiro-<role>-<version>-<rid>.tar.gz(附 .sha256)
|
||||
# 打包:scripts/package.sh <agent> <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
|
||||
role="${1:?usage: package.sh <agent|probe> <rid>}"
|
||||
rid="${2:?usage: package.sh <agent|probe> <rid>}"
|
||||
role="${1:?usage: package.sh <agent> <rid>}"
|
||||
rid="${2:?usage: package.sh <agent> <rid>}"
|
||||
[[ "$role" == agent ]] || { echo "only agent packages are supported" >&2; exit 2; }
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="$(sed -n 's#.*<Version>\(.*\)</Version>.*#\1#p' "$root/Directory.Build.props" | head -1)"
|
||||
version="$(sed -n 's#.*<Version>\(.*\)</Version>.*#\1#p' "$root/backend/Directory.Build.props" | head -1)"
|
||||
name="moshiro-$role-$version-$rid"
|
||||
stage="$root/backend/artifacts/packages/stage/$name"
|
||||
quic_runtime="$root/backend/artifacts/packages/quic-runtime/$rid"
|
||||
@@ -27,10 +28,11 @@ fi
|
||||
cp "$root/backend/artifacts/publish/$role/$rid/moshiro-$role" "$stage/"
|
||||
cp "$root/packaging/install.sh" "$stage/"
|
||||
cp "$root/packaging/systemd/moshiro-$role.service" "$stage/systemd/"
|
||||
cp "$root/$role/README.md" "$stage/README.md"
|
||||
cp "$root/backend/$role/README.md" "$stage/README.md"
|
||||
if [ "$role" = agent ]; then
|
||||
cp "$root/backend/artifacts/publish/fleet-enroll/$rid/fleet-enroll" "$stage/"
|
||||
else
|
||||
fi
|
||||
if [[ "$rid" == linux-* ]]; then
|
||||
mkdir -p "$stage/openwrt"
|
||||
cp "$root/packaging/openwrt/"* "$stage/openwrt/"
|
||||
fi
|
||||
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# 发布一个节点程序:scripts/publish.sh <agent|probe|backend|fleet-enroll> <rid>
|
||||
# 发布一个节点程序:scripts/publish.sh <agent|backend|fleet-enroll> <rid>
|
||||
# 输出到 backend/artifacts/publish/<project>/<rid>/。linux-musl-x64(Alpine)和 linux-x64(Ubuntu noble,glibc)在 NativeAOT SDK 容器里构建(任何主机都可以,包括 Windows),
|
||||
# 其他 rid 用本机的 dotnet(NativeAOT 不能交叉编译到别的操作系统,所以 win-x64 要在 Windows 上、linux-x64 要在 glibc 的 Linux 上构建)。
|
||||
set -euo pipefail
|
||||
|
||||
project="${1:?usage: publish.sh <agent|probe|backend|fleet-enroll> <rid>}"
|
||||
rid="${2:?usage: publish.sh <agent|probe|backend|fleet-enroll> <rid>}"
|
||||
project="${1:?usage: publish.sh <agent|backend|fleet-enroll> <rid>}"
|
||||
rid="${2:?usage: publish.sh <agent|backend|fleet-enroll> <rid>}"
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
case "$project" in
|
||||
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 ;;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 在 Linux 容器里用发布出来的 NativeAOT 二进制跑一遍完整链路:后端 → 注册 Agent → Agent → Probe → 数据落库。
|
||||
#
|
||||
# RID=linux-musl-x64 IMAGE=alpine:3.22 (默认)
|
||||
# RID=linux-x64 IMAGE=debian:bookworm-slim
|
||||
# MODE=install 用打包产物里的 install.sh 安装(agent 注册、probe 身份、以 moshiro 用户运行),而不是直接运行二进制
|
||||
#
|
||||
# 用法(在仓库根目录;Windows 下先 export MSYS_NO_PATHCONV=1;需先运行 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 [秒数]
|
||||
set -eu
|
||||
seconds="${1:-60}"
|
||||
rid="${RID:-linux-musl-x64}"
|
||||
mode="${MODE:-direct}"
|
||||
pub=/art/publish
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache curl jq libssl3 libstdc++ >/dev/null
|
||||
else
|
||||
apt-get update >/dev/null && apt-get install -y --no-install-recommends curl jq ca-certificates libssl3 >/dev/null
|
||||
fi
|
||||
|
||||
work=/tmp/smoke
|
||||
mkdir -p "$work/data" "$work/probe"
|
||||
base=http://127.0.0.1:18080
|
||||
|
||||
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"
|
||||
|
||||
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')
|
||||
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":"smoke","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)
|
||||
probe_join=$(curl -fs -X POST "$base/api/admin/join-tokens" -H "$auth" -H 'Content-Type: application/json' -d '{"role":"probe","max_uses":1,"expires_in_hours":1}' | jq -r .plaintext)
|
||||
|
||||
if [ "$mode" = install ]; then
|
||||
# 安装到 /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
|
||||
(cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --server "$base" --join "$probe_join" --name smoke-probe --no-start)
|
||||
run_as /usr/lib/moshiro/moshiro-probe --config /etc/moshiro/probe.json >"$work/probe.log" 2>&1 &
|
||||
probe_pid=$!
|
||||
else
|
||||
for role in agent probe; do
|
||||
package_dir="$work/$role-bin"
|
||||
mkdir -p "$package_dir"
|
||||
set -- /art/packages/moshiro-"$role"-*-"$rid".tar.gz
|
||||
[ -f "$1" ] || { echo "missing QUIC-bundled package: $1 (run scripts/package.sh first)" >&2; exit 1; }
|
||||
tar -xzf "$1" -C "$package_dir"
|
||||
done
|
||||
mkdir -p "$work/agent-id"
|
||||
"$work/agent-bin/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"
|
||||
"$work/agent-bin/moshiro-agent" --config "$work/agent.json" >"$work/agent.log" 2>&1 &
|
||||
agent_pid=$!
|
||||
sleep 3
|
||||
mkdir -p "$work/probe-id"
|
||||
"$work/probe-bin/fleet-enroll" --server "$base" --join "$probe_join" --name smoke-probe --identity-dir "$work/probe-id"
|
||||
printf '{"fleet":{"server":"%s","identity_dir":"%s"}}' "$base" "$work/probe-id" >"$work/probe/probe.json"
|
||||
"$work/probe-bin/moshiro-probe" --config "$work/probe/probe.json" >"$work/probe.log" 2>&1 &
|
||||
probe_pid=$!
|
||||
fi
|
||||
|
||||
rss() { awk '/VmRSS/ {printf "%.1f", $2/1024}' "/proc/$1/status"; }
|
||||
i=0
|
||||
while [ "$i" -lt "$seconds" ]; do
|
||||
sleep 10; i=$((i + 10))
|
||||
echo "t+${i}s probe=$(rss "$probe_pid")MB agent=$(rss "$agent_pid")MB"
|
||||
done
|
||||
|
||||
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] | group_by(.) | map({(.[0]): length}) | add')"
|
||||
agent_id=$(curl -fs "$base/api/admin/agents" -H "$auth" | jq -r '.items[0].id')
|
||||
echo "tcp summary: $(curl -fs "$base/api/public/lines/$agent_id/series?range=5m&step=1s&metrics=tcp.rtt.p50,tcp.rtt.p999,tcp.rtt.max,tcp.rtt.tail,tcp.slow,tcp.retrans,tcp.srtt.p95" -H "$auth" | jq -c '.summary.metrics')"
|
||||
echo "--- probe.log"; tail -4 "$work/probe.log"
|
||||
echo "--- agent.log"; tail -4 "$work/agent.log"
|
||||
@@ -1,31 +0,0 @@
|
||||
# 在带 systemd 的 Debian 容器里用 install.sh 安装并以 systemd 单元运行(验证沙箱指令与 NativeAOT 兼容)。
|
||||
# docker run -d --name sysd --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw -v "$PWD/backend/artifacts:/art:ro" jrei/systemd-debian:12
|
||||
# docker cp scripts/smoke-systemd.sh sysd:/s.sh && docker exec sysd bash /s.sh
|
||||
set -eu
|
||||
apt-get update >/dev/null && apt-get install -y --no-install-recommends curl jq ca-certificates libssl3 >/dev/null
|
||||
base=http://127.0.0.1:18080
|
||||
mkdir -p /tmp/data
|
||||
cd /art/publish/backend/linux-x64
|
||||
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')
|
||||
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/moshiro-$r-*-linux-x64.tar.gz; done
|
||||
(cd /tmp/pkg/moshiro-agent-* && ./install.sh agent --server $base --join "$join" --name sysd-agent)
|
||||
sleep 3
|
||||
probe_join=$(curl -fs -X POST $base/api/admin/join-tokens -H "$auth" -H 'Content-Type: application/json' -d '{"role":"probe","max_uses":1,"expires_in_hours":1}' | jq -r .plaintext)
|
||||
(cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --server $base --join "$probe_join" --name sysd-probe)
|
||||
sleep 25
|
||||
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 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
|
||||
@@ -1,77 +0,0 @@
|
||||
"""跨机器的真实链路验证: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/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/ops/deployment.md 第 3 节。
|
||||
"""
|
||||
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\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
|
||||
|
||||
|
||||
def wsl(cmd, check=True):
|
||||
r = subprocess.run(["wsl", "-d", "Debian", "-u", "root", "-e", "bash", "-c", cmd], capture_output=True, text=True)
|
||||
if check and r.returncode != 0:
|
||||
print("WSL FAILED:", cmd, r.stdout, r.stderr)
|
||||
raise SystemExit(1)
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
WSLIP = wsl("ip -4 addr show eth0 | grep -oP '(?<=inet )[0-9.]+'")
|
||||
BASE = f"http://{WSLIP}:{PORT}"
|
||||
|
||||
|
||||
def req(method, path, body=None, headers=None, ctype="application/json"):
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
h = {"Content-Type": ctype}
|
||||
h.update(headers or {})
|
||||
r = urllib.request.Request(BASE + path, data=data, method=method, headers=h)
|
||||
try:
|
||||
with OPENER.open(r, timeout=30) as resp:
|
||||
text = resp.read().decode()
|
||||
return resp, (json.loads(text) if text else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
print("HTTP", e.code, e.read().decode())
|
||||
raise
|
||||
|
||||
|
||||
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("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 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 ./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]
|
||||
print("agent", agent["id"], agent["status"], "online", agent.get("online"))
|
||||
# the backend only sees 127.0.0.1 as the agent's address: the panel needs the address the probe can reach
|
||||
req("PATCH", "/api/admin/agents/" + agent["id"], {"public_host": WSLIP}, auth, "application/merge-patch+json")
|
||||
_, ident = req("POST", "/api/admin/probes", {"name": "home"}, auth)
|
||||
print("bootstrap", ident["bootstrap"])
|
||||
os.makedirs(WORK + r"\probe", exist_ok=True)
|
||||
json.dump(ident, open(WORK + r"\probe\probe.json", "w"))
|
||||
json.dump({"base": BASE, "token": tok["plaintext"], "wslip": WSLIP, "agent_id": agent["id"]}, open(WORK + r"\state.json", "w"))
|
||||
log = open(WORK + r"\probe.log", "w")
|
||||
p = subprocess.Popen([PROBE_EXE, "--config", WORK + r"\probe\probe.json"], stdout=log, stderr=subprocess.STDOUT)
|
||||
open(WORK + r"\probe.pid", "w").write(str(p.pid))
|
||||
print("probe pid", p.pid)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,613 +0,0 @@
|
||||
"""
|
||||
稳定性实测:真实的初始化、正常关闭、崩溃关闭、链路故障与恢复。
|
||||
|
||||
拓扑:Windows 上的 Probe(NativeAOT exe)→ WSL2 里的 Agent → 同一个 WSL 里的 Backend。
|
||||
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/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
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
# 要连的是本机的虚拟网络(WSL),不能走环境变量里的 HTTP(S)_PROXY
|
||||
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", "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("MOSHIRO_ADMIN_PASSWORD", "correct horse battery") # 测试用后端的初始管理员密码
|
||||
RESULTS = []
|
||||
HIDDEN = subprocess.CREATE_NO_WINDOW # 所有子进程都不弹窗
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ WSL / process helpers
|
||||
|
||||
def wsl(cmd, check=True, timeout=120):
|
||||
r = subprocess.run(["wsl", "-d", "Debian", "-u", "root", "-e", "bash", "-c", cmd], capture_output=True, text=True, timeout=timeout, creationflags=HIDDEN)
|
||||
if check and r.returncode != 0:
|
||||
raise RuntimeError(f"wsl failed ({r.returncode}): {cmd}\n{r.stdout}\n{r.stderr}")
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
WSLIP = wsl("ip -4 addr show eth0 | grep -oP '(?<=inet )[0-9.]+'")
|
||||
BASE = f"http://{WSLIP}:{PORT}"
|
||||
TOKEN = ""
|
||||
|
||||
LAUNCHER = r"""#!/bin/sh
|
||||
# start.sh <name> <command...>: run detached, append to <name>.log, record the exit code in <name>.exit
|
||||
name=$1; shift
|
||||
rm -f /root/mon/$name.exit
|
||||
setsid -f sh -c 'cd /root/mon; "$@" >> /root/mon/'$name'.log 2>&1; echo $? > /root/mon/'$name'.exit' sh "$@" < /dev/null
|
||||
"""
|
||||
|
||||
|
||||
def now():
|
||||
return time.time()
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
RESULTS.append((name, bool(ok), detail))
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" - {detail}" if detail else ""), flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
def wait_until(cond, timeout, step=0.5):
|
||||
start = now()
|
||||
while now() - start < timeout:
|
||||
try:
|
||||
if cond():
|
||||
return now() - start
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(step)
|
||||
return None
|
||||
|
||||
|
||||
def api(method, path, body=None, ctype="application/json", timeout=15):
|
||||
headers = {"Content-Type": ctype}
|
||||
if TOKEN:
|
||||
headers["Authorization"] = "Bearer " + TOKEN
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
req = urllib.request.Request(BASE + path, data=data, method=method, headers=headers)
|
||||
with OPENER.open(req, timeout=timeout) as resp:
|
||||
text = resp.read().decode()
|
||||
return json.loads(text) if text else None
|
||||
|
||||
|
||||
def backend_up():
|
||||
try:
|
||||
OPENER.open(BASE + "/healthz", timeout=2)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ components
|
||||
|
||||
def pids(name):
|
||||
out = wsl(f"pgrep -x {name} || true")
|
||||
return [int(x) for x in out.split()]
|
||||
|
||||
|
||||
def rss_mb(name):
|
||||
p = pids(name)
|
||||
if not p:
|
||||
return None
|
||||
return int(wsl(f"ps -o rss= -p {p[0]}")) / 1024
|
||||
|
||||
|
||||
def start_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 ./moshiro-agent --config /root/mon/agent.json")
|
||||
return wait_until(lambda: bool(pids("moshiro-agent")), 10)
|
||||
|
||||
|
||||
def stop_proc(name, sig="TERM", timeout=30):
|
||||
"""Send a signal and wait for the process to exit; returns (seconds, exit code or None)."""
|
||||
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('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)
|
||||
|
||||
|
||||
def send_ctrl_break(pid):
|
||||
"""
|
||||
Ctrl+Break to a process that owns its own (hidden) console: attach to that console, broadcast the event, detach.
|
||||
Ctrl+Break rather than Ctrl+C: a console process can inherit "ignore Ctrl+C" from its parent (it does under most job runners),
|
||||
Ctrl+Break cannot be ignored that way. .NET reports it as SIGQUIT.
|
||||
"""
|
||||
from ctypes import wintypes
|
||||
k = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
handler = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)(lambda event: True) # swallow the event in this process
|
||||
k.FreeConsole()
|
||||
if not k.AttachConsole(pid):
|
||||
raise OSError(f"AttachConsole({pid}) failed: {ctypes.get_last_error()}")
|
||||
k.SetConsoleCtrlHandler(handler, True)
|
||||
k.GenerateConsoleCtrlEvent(1, 0)
|
||||
time.sleep(0.3)
|
||||
k.FreeConsole()
|
||||
k.SetConsoleCtrlHandler(handler, False)
|
||||
|
||||
|
||||
class Probe:
|
||||
def __init__(self):
|
||||
self.proc = None
|
||||
self.log = os.path.join(WORK, "probe.log")
|
||||
|
||||
def start(self):
|
||||
os.makedirs(WORK, exist_ok=True)
|
||||
handle = open(self.log, "a")
|
||||
# 自己的隐藏控制台:这样才能从外面给它发 Ctrl+Break
|
||||
flags = subprocess.CREATE_NO_WINDOW
|
||||
self.proc = subprocess.Popen([PROBE_EXE, "--config", os.path.join(WORK, "probe", "probe.json")], stdout=handle, stderr=subprocess.STDOUT, creationflags=flags)
|
||||
return self.proc.pid
|
||||
|
||||
def alive(self):
|
||||
return self.proc is not None and self.proc.poll() is None
|
||||
|
||||
def stop(self, mode="graceful", timeout=20):
|
||||
if not self.alive():
|
||||
return 0.0, self.proc.poll() if self.proc else None
|
||||
start = now()
|
||||
if mode == "graceful":
|
||||
send_ctrl_break(self.proc.pid)
|
||||
else:
|
||||
self.proc.kill()
|
||||
try:
|
||||
code = self.proc.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
return None, None
|
||||
return now() - start, code
|
||||
|
||||
def suspend(self):
|
||||
"""Freeze the process (its TCP connections stay open and go silent, like a hung machine)."""
|
||||
handle = ctypes.windll.kernel32.OpenProcess(0x0800, False, self.proc.pid) # PROCESS_SUSPEND_RESUME
|
||||
ctypes.windll.ntdll.NtSuspendProcess(handle)
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
|
||||
def resume(self):
|
||||
handle = ctypes.windll.kernel32.OpenProcess(0x0800, False, self.proc.pid)
|
||||
ctypes.windll.ntdll.NtResumeProcess(handle)
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
|
||||
def working_set_mb(self):
|
||||
if not self.alive():
|
||||
return None
|
||||
out = subprocess.run(["powershell", "-NoProfile", "-Command", f"(Get-Process -Id {self.proc.pid}).WorkingSet64"], capture_output=True, text=True, creationflags=HIDDEN).stdout.strip()
|
||||
return int(out) / 1048576 if out else None
|
||||
|
||||
def log_text(self):
|
||||
return open(self.log, encoding="utf-8", errors="replace").read() if os.path.exists(self.log) else ""
|
||||
|
||||
|
||||
PROBE = Probe()
|
||||
AGENT_ID = ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ environment setup
|
||||
|
||||
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 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/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)
|
||||
os.makedirs(os.path.join(WORK, "probe"), exist_ok=True)
|
||||
assert start_backend() is not None, "backend did not start"
|
||||
if fresh:
|
||||
req = urllib.request.Request(BASE + "/api/admin/auth/login", data=json.dumps({"username": "admin", "password": ADMIN_PASSWORD}).encode(), headers={"Content-Type": "application/json"}, method="POST")
|
||||
with OPENER.open(req, timeout=15) as resp:
|
||||
cookie = resp.headers["Set-Cookie"].split(";")[0]
|
||||
csrf = json.loads(resp.read())["csrf_token"]
|
||||
req = urllib.request.Request(BASE + "/api/admin/api-tokens", data=json.dumps({"name": "stab", "scope": "admin"}).encode(), headers={"Content-Type": "application/json", "Cookie": cookie, "X-CSRF-Token": csrf}, method="POST")
|
||||
TOKEN = json.loads(OPENER.open(req, timeout=15).read())["plaintext"]
|
||||
join = api("POST", "/api/admin/join-tokens", {"auto_approve": True, "max_uses": 5, "expires_in_hours": 1})["plaintext"]
|
||||
wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join {join} --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")
|
||||
assert start_agent() is not None
|
||||
assert wait_until(lambda: api("GET", "/api/admin/agents")["items"][0]["online"], 20) is not None, "agent did not come online"
|
||||
agent = api("GET", "/api/admin/agents")["items"][0]
|
||||
AGENT_ID = agent["id"]
|
||||
if fresh:
|
||||
api("PATCH", "/api/admin/agents/" + AGENT_ID, {"public_host": WSLIP}, "application/merge-patch+json")
|
||||
ident = api("POST", "/api/admin/probes", {"name": "home"})
|
||||
json.dump(ident, open(os.path.join(WORK, "probe", "probe.json"), "w"))
|
||||
PROBE.start()
|
||||
assert wait_until(agent_view_probe_connected, 30) is not None, "probe did not connect"
|
||||
|
||||
|
||||
def agent_view():
|
||||
return api("GET", "/api/admin/agents/" + AGENT_ID)
|
||||
|
||||
|
||||
def agent_view_probe_connected():
|
||||
return agent_view()["probe_connected"]
|
||||
|
||||
|
||||
def agent_online():
|
||||
return agent_view()["online"]
|
||||
|
||||
|
||||
def metrics(rng="2m", names="loss.total,loss.up,loss.down,rtt.p50,tcp.rtt.p50,tcp.rtt.max"):
|
||||
s = api("GET", f"/api/public/lines/{AGENT_ID}/series?range={rng}&step=1s&metrics={names}")
|
||||
return s["summary"]["metrics"], s["summary"]["samples"]
|
||||
|
||||
|
||||
def gaps(minutes=3):
|
||||
"""Buckets (10 s) in the last N minutes, after the first data, whose udp sample count is below half of the expected 100."""
|
||||
s = api("GET", f"/api/public/lines/{AGENT_ID}/series?range={minutes}m&step=10s&metrics=n.udp_echo,n.tcp_echo")
|
||||
udp = s["values"]["n.udp_echo"]
|
||||
first = next((i for i, v in enumerate(udp) if v is not None and v >= 50), len(udp)) # buckets before the first data are not gaps
|
||||
return [i for i, v in enumerate(udp[:-1]) if i > first and (v is None or v < 50)], len(udp)
|
||||
|
||||
|
||||
def stored_chunks():
|
||||
return len(api("GET", f"/api/admin/raw/chunks?agent_id={AGENT_ID}&limit=1000")["items"])
|
||||
|
||||
|
||||
def healthy():
|
||||
try:
|
||||
return agent_online() and agent_view_probe_connected()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_healthy(timeout=60):
|
||||
if not backend_up():
|
||||
start_backend()
|
||||
if not pids("moshiro-agent"):
|
||||
start_agent()
|
||||
if not PROBE.alive():
|
||||
PROBE.start()
|
||||
return wait_until(healthy, timeout)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ scenarios
|
||||
|
||||
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 moshiro-agent; pkill -9 -x moshiro-backend; true")
|
||||
time.sleep(1)
|
||||
PROBE.start()
|
||||
time.sleep(3)
|
||||
start_agent()
|
||||
time.sleep(3)
|
||||
t0 = now()
|
||||
start_backend()
|
||||
took = wait_until(healthy, 90)
|
||||
check("probe -> agent -> backend start order converges", took is not None, f"{took:.1f}s after the backend came up" if took else "never became healthy")
|
||||
chunks_flow = wait_until(lambda: stored_chunks() > 0, 40)
|
||||
check("data flows after the cold start", chunks_flow is not None)
|
||||
|
||||
# 2. backend first, then agent, then probe (the normal order) after a full stop
|
||||
PROBE.stop("kill")
|
||||
wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; true")
|
||||
start_backend()
|
||||
start_agent()
|
||||
PROBE.start()
|
||||
took = wait_until(healthy, 60)
|
||||
check("backend -> agent -> probe start order converges", took is not None, f"{took:.1f}s" if took else "")
|
||||
|
||||
# 3. the probe starts when its agent list is only a stale cache (agents.json wrong): bootstrap is still tried
|
||||
# 4. bad configuration: clear errors and a non-zero exit code, no crash dump
|
||||
bad = os.path.join(WORK, "bad.json")
|
||||
open(bad, "w").write("{ not json")
|
||||
r = subprocess.run([PROBE_EXE, "--config", bad], capture_output=True, text=True, timeout=20, creationflags=HIDDEN)
|
||||
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 && ./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 && ./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:])
|
||||
out = wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join bogus --name x --identity-dir /root/mon/id 2>&1; echo exit=$?", check=False)
|
||||
check("fleet-enroll: refuses to overwrite an existing identity", "exit=0" not in out and "already enrolled" in out, out.strip()[-140:])
|
||||
ensure_healthy()
|
||||
|
||||
|
||||
def scenario_graceful():
|
||||
print("== graceful: SIGTERM / Ctrl+Break of each component, then restart")
|
||||
ensure_healthy()
|
||||
# probe (Ctrl+Break in its own console)
|
||||
secs, code = PROBE.stop("graceful")
|
||||
check("probe exits on Ctrl+Break 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}")
|
||||
seen = wait_until(lambda: not agent_view_probe_connected(), 10)
|
||||
check("backend sees the probe disconnect", seen is not None, f"{seen and round(seen, 1)}s")
|
||||
PROBE.start()
|
||||
check("probe restarts and reconnects", wait_until(healthy, 30) is not None)
|
||||
|
||||
# agent
|
||||
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 "")
|
||||
seen = wait_until(lambda: not agent_online(), 10)
|
||||
check("backend sees the agent go offline", seen is not None, f"{seen and round(seen, 1)}s")
|
||||
start_agent()
|
||||
check("agent restarts, reconnects to backend and probe", wait_until(healthy, 40) is not None)
|
||||
|
||||
# backend
|
||||
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:])
|
||||
time.sleep(5)
|
||||
start_backend()
|
||||
check("backend restarts, agent and probe converge again", wait_until(healthy, 60) is not None)
|
||||
ensure_healthy()
|
||||
|
||||
|
||||
def scenario_crash():
|
||||
print("== crash: kill -9 / taskkill /F of each component, data durability")
|
||||
ensure_healthy()
|
||||
time.sleep(25)
|
||||
before = stored_chunks()
|
||||
|
||||
# backend crash: the agent keeps its outbox; after restart nothing acknowledged is lost
|
||||
t = now()
|
||||
wsl("pkill -9 -x moshiro-backend")
|
||||
time.sleep(25) # agent outbox accumulates (2-3 chunk rounds)
|
||||
start_backend()
|
||||
back = wait_until(healthy, 60)
|
||||
check("backend kill -9: agent reconnects and everything converges", back is not None, f"{back and round(back, 1)}s after restart")
|
||||
time.sleep(20)
|
||||
after = stored_chunks()
|
||||
g, n = gaps(3)
|
||||
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 moshiro-agent")
|
||||
time.sleep(5)
|
||||
start_agent()
|
||||
check("agent kill -9: recovers", wait_until(healthy, 40) is not None)
|
||||
|
||||
# probe crash
|
||||
PROBE.stop("kill")
|
||||
time.sleep(5)
|
||||
PROBE.start()
|
||||
check("probe kill: recovers", wait_until(healthy, 40) is not None)
|
||||
time.sleep(25)
|
||||
m, samples = metrics("30s")
|
||||
check("after all three crashes the line reports correct data (no phantom loss)", (m.get("loss.up") or 0) < 1 and (m.get("loss.down") or 0) < 1, f"loss.up={m.get('loss.up')} loss.down={m.get('loss.down')}")
|
||||
|
||||
# crash in the middle of an on-demand test
|
||||
test = api("POST", f"/api/admin/lines/{AGENT_ID}/tests", {"plan": "hy2_ladder", "params": {"size": 1250}})
|
||||
time.sleep(12)
|
||||
PROBE.stop("kill")
|
||||
state = wait_until(lambda: api("GET", "/api/public/tests/" + test["id"])["status"] in ("failed", "aborted"), 30)
|
||||
final = api("GET", "/api/public/tests/" + test["id"])
|
||||
check("probe crash mid-test: the test ends (failed/aborted), it does not hang", state is not None, f"status={final['status']} error={final.get('error_code')}")
|
||||
PROBE.start()
|
||||
wait_until(healthy, 40)
|
||||
test2 = api("POST", f"/api/admin/lines/{AGENT_ID}/tests", {"plan": "hy2_ladder", "params": {"size": 1250}})
|
||||
done = wait_until(lambda: api("GET", "/api/public/tests/" + test2["id"])["status"] in ("succeeded", "failed", "aborted"), 90, 2)
|
||||
check("a new test runs to completion after the crash (the agent is not stuck busy)", done is not None and api("GET", "/api/public/tests/" + test2["id"])["status"] == "succeeded", api("GET", "/api/public/tests/" + test2["id"])["status"])
|
||||
ensure_healthy()
|
||||
|
||||
|
||||
def netem(device, spec, direction="egress", ports=(7443, 7444)):
|
||||
"""Impair only the data/control ports of the agent (so the rest of WSL is unaffected)."""
|
||||
clear_netem(device)
|
||||
if direction == "egress":
|
||||
wsl(f"tc qdisc add dev {device} root handle 1: prio && tc qdisc add dev {device} parent 1:3 handle 30: netem {spec}")
|
||||
for p in ports:
|
||||
wsl(f"tc filter add dev {device} protocol ip parent 1:0 prio 3 u32 match ip sport {p} 0xffff flowid 1:3")
|
||||
else:
|
||||
wsl(f"tc qdisc add dev {device} handle ffff: ingress")
|
||||
for p in ports:
|
||||
action = "action drop" if spec == "drop" else ""
|
||||
wsl(f"tc filter add dev {device} parent ffff: protocol ip u32 match ip dport {p} 0xffff {action}")
|
||||
|
||||
|
||||
def clear_netem(device):
|
||||
wsl(f"tc qdisc del dev {device} root 2>/dev/null; tc qdisc del dev {device} ingress 2>/dev/null; true")
|
||||
|
||||
|
||||
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("moshiro-agent"))
|
||||
|
||||
# 1. flapping: 3 s blackholes every 10 s, 8 times
|
||||
for _ in range(8):
|
||||
netem("eth0", "drop", "ingress")
|
||||
time.sleep(3)
|
||||
clear_netem("eth0")
|
||||
time.sleep(7)
|
||||
check("flapping link: probe still connected afterwards", wait_until(healthy, 30) is not None)
|
||||
|
||||
# 2. a 70 s blackhole: both ends must notice or survive, and recover once the path returns
|
||||
t = now()
|
||||
netem("eth0", "drop", "ingress")
|
||||
time.sleep(70)
|
||||
clear_netem("eth0")
|
||||
back = wait_until(healthy, 120)
|
||||
check("70 s blackhole: converges back to healthy", back is not None, f"{back and round(back, 1)}s after the path returned")
|
||||
time.sleep(35)
|
||||
m, _ = metrics("30s")
|
||||
check("after the blackhole the line is measured correctly again", (m.get("loss.total") or 0) < 2, f"loss.total={m.get('loss.total')}")
|
||||
|
||||
# 3. reconnect storm: kill the agent 12 times in a row
|
||||
for _ in range(12):
|
||||
wsl("pkill -9 -x moshiro-agent; true")
|
||||
time.sleep(0.5)
|
||||
start_agent()
|
||||
time.sleep(2)
|
||||
check("12 agent restarts in a row: converges", wait_until(healthy, 60) is not None)
|
||||
time.sleep(30)
|
||||
g, n = gaps(1)
|
||||
check("... and the data is flowing again", not g, f"gaps: {g}")
|
||||
|
||||
# 4. restart storm of the probe
|
||||
for _ in range(8):
|
||||
PROBE.stop("kill")
|
||||
time.sleep(0.3)
|
||||
PROBE.start()
|
||||
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("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()
|
||||
|
||||
|
||||
def scenario_backend_link():
|
||||
print("== agent<->backend link: outage buffering, replay, duplicates")
|
||||
ensure_healthy()
|
||||
time.sleep(30)
|
||||
# an outage on loopback (tc on lo: the agent<->backend link) shorter than the buffering capacity
|
||||
netem("lo", "drop", "ingress", ports=(PORT,))
|
||||
t = now()
|
||||
time.sleep(45)
|
||||
clear_netem("lo")
|
||||
check("45 s agent<->backend outage: agent stays up and reconnects", wait_until(agent_online, 90) is not None, f"{now() - t:.0f}s")
|
||||
time.sleep(35)
|
||||
g, n = gaps(4)
|
||||
check("data produced during the outage is replayed from the agent's outbox (no gap)", not g, f"gaps in {n} buckets: {g}")
|
||||
|
||||
# duplicates: chunk ids are idempotent. The count of stored chunks grows by the expected 2 per 10 s, not more
|
||||
c0 = stored_chunks()
|
||||
time.sleep(30)
|
||||
c1 = stored_chunks()
|
||||
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("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)
|
||||
time.sleep(35)
|
||||
g, n = gaps(4)
|
||||
check("... with no gap in the data", not g, f"gaps: {g}")
|
||||
ensure_healthy()
|
||||
|
||||
|
||||
def scenario_silent():
|
||||
print("== silent death: a frozen process keeps its sockets open and says nothing (hung machine, lost power)")
|
||||
ensure_healthy()
|
||||
time.sleep(10)
|
||||
|
||||
# a frozen probe: the agent must notice and tell the backend, instead of keeping a dead session forever
|
||||
PROBE.suspend()
|
||||
t = now()
|
||||
seen = wait_until(lambda: not agent_view_probe_connected(), 150, 1)
|
||||
check("frozen probe: the backend learns the probe is gone", seen is not None, f"{seen and round(seen)}s" if seen else "still shown as connected after 150 s")
|
||||
PROBE.resume()
|
||||
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 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 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 moshiro-backend")
|
||||
time.sleep(70)
|
||||
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)
|
||||
check("... and the data produced while it was frozen is delivered", not g, f"gaps: {g}")
|
||||
ensure_healthy()
|
||||
|
||||
|
||||
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("moshiro-agent"), rss_mb("moshiro-backend"))
|
||||
import random
|
||||
rnd = random.Random(7)
|
||||
end = now() + 8 * 60
|
||||
faults = 0
|
||||
while now() < end:
|
||||
fault = rnd.choice(["blackhole", "loss", "agent_kill", "probe_kill", "none", "none"])
|
||||
faults += fault != "none"
|
||||
if fault == "blackhole":
|
||||
netem("eth0", "drop", "ingress")
|
||||
time.sleep(rnd.uniform(1, 6))
|
||||
clear_netem("eth0")
|
||||
elif fault == "loss":
|
||||
netem("eth0", "delay 20ms 5ms loss 5%", "egress")
|
||||
time.sleep(rnd.uniform(5, 12))
|
||||
clear_netem("eth0")
|
||||
elif fault == "agent_kill":
|
||||
wsl("pkill -9 -x moshiro-agent; true")
|
||||
time.sleep(1)
|
||||
start_agent()
|
||||
elif fault == "probe_kill":
|
||||
PROBE.stop("kill")
|
||||
time.sleep(0.5)
|
||||
PROBE.start()
|
||||
time.sleep(rnd.uniform(8, 20))
|
||||
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("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')}")
|
||||
|
||||
|
||||
SCENARIOS = {
|
||||
"init": scenario_init,
|
||||
"graceful": scenario_graceful,
|
||||
"crash": scenario_crash,
|
||||
"probe_link": scenario_probe_link,
|
||||
"backend_link": scenario_backend_link,
|
||||
"silent": scenario_silent,
|
||||
"soak": scenario_soak,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
names = sys.argv[1:] or list(SCENARIOS)
|
||||
print(f"wsl ip {WSLIP}")
|
||||
setup(fresh=True)
|
||||
for name in names:
|
||||
try:
|
||||
SCENARIOS[name]()
|
||||
except Exception as ex:
|
||||
check(f"{name} ran to completion", False, repr(ex)[:300])
|
||||
PROBE.stop("kill")
|
||||
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:
|
||||
print(f"FAILED: {name} - {detail}")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -44,11 +44,11 @@ elif name == 'jsonfilter':
|
||||
args = sys.argv[1:]
|
||||
data = json.loads(pathlib.Path(args[args.index('-i') + 1]).read_text() if '-i' in args else sys.stdin.read())
|
||||
query = args[args.index('-e') + 1]
|
||||
value = data['moshiro-probe']['instances']['default']['pid'] if query.startswith('@[') else data.get(query[2:])
|
||||
value = data['moshiro-agent']['instances']['default']['pid'] if query.startswith('@[') else data.get(query[2:])
|
||||
if value is None: sys.exit(1)
|
||||
print(value)
|
||||
elif name == 'ubus':
|
||||
print(json.dumps({'moshiro-probe': {'instances': {'default': {'pid': 1234}}}}))
|
||||
print(json.dumps({'moshiro-agent': {'instances': {'default': {'pid': 1234}}}}))
|
||||
elif name == 'service':
|
||||
with (base / 'restarts').open('a') as f: f.write('restart\n')
|
||||
if os.environ.get('HEALTH') != 'good': sys.exit(0)
|
||||
@@ -93,7 +93,7 @@ class UpdaterTests(unittest.TestCase):
|
||||
|
||||
def publish(self, *, seq=100, version="1.0.1-dev.1", members=None, changes=None):
|
||||
payload = io.BytesIO()
|
||||
members = members or {name: b"executable\n" for name in ("moshiro-probe", "fleet-enroll", "openwrt.sh", "probe.init", "updater.init")}
|
||||
members = members or {name: b"executable\n" for name in ("moshiro-agent", "fleet-enroll", "openwrt.sh", "agent.init", "updater.init")}
|
||||
with tarfile.open(fileobj=payload, mode="w:gz") as archive:
|
||||
for name, content in members.items():
|
||||
entry = tarfile.TarInfo(name)
|
||||
@@ -101,11 +101,11 @@ class UpdaterTests(unittest.TestCase):
|
||||
archive.addfile(entry, io.BytesIO(content))
|
||||
data = payload.getvalue()
|
||||
self.hash = hashlib.sha256(data).hexdigest()
|
||||
package = f"probe-linux-musl-arm64-{self.hash}.tar.gz"
|
||||
package = f"agent-linux-musl-arm64-{self.hash}.tar.gz"
|
||||
version_dir = self.base / "feed" / version
|
||||
version_dir.mkdir(exist_ok=True)
|
||||
(version_dir / package).write_bytes(data)
|
||||
self.manifest = dict(schema=1, component="probe", channel="dev", rid="linux-musl-arm64", version=version,
|
||||
self.manifest = dict(schema=1, component="agent", channel="dev", rid="linux-musl-arm64", version=version,
|
||||
package=package, sha256=self.hash, size=len(data), seq=seq)
|
||||
self.manifest.update(changes or {})
|
||||
self.sign()
|
||||
@@ -144,7 +144,7 @@ class UpdaterTests(unittest.TestCase):
|
||||
def test_ci_publisher_produces_a_signed_package_the_updater_can_install(self):
|
||||
binaries = self.base / "binaries"
|
||||
binaries.mkdir()
|
||||
for name in ("moshiro-probe", "fleet-enroll"):
|
||||
for name in ("moshiro-agent", "fleet-enroll"):
|
||||
(binaries / name).write_bytes(b"native executable fixture\n")
|
||||
env = dict(self.env, CI_COMMIT_TAG="", CI_PIPELINE_EVENT="push", CI_COMMIT_BRANCH="main", CI_COMMIT_MESSAGE="[CI] fixture", GITEA_BASE="https://test.invalid",
|
||||
GITEA_TOKEN="test-token", RELEASE_SIGNING_KEY=self.key.read_text())
|
||||
|
||||
Reference in new issue
Block a user