diff --git a/agent/go/cmd/rmm-agent/main.go b/agent/go/cmd/rmm-agent/main.go index 7275a39..55929f7 100644 --- a/agent/go/cmd/rmm-agent/main.go +++ b/agent/go/cmd/rmm-agent/main.go @@ -10,7 +10,9 @@ import ( "flag" "fmt" "io" + "net" "net/http" + "net/url" "os" "os/exec" "os/signal" @@ -266,7 +268,7 @@ func heartbeatOnce(ctx context.Context, client *http.Client, cfg config) error { body := map[string]any{ "device_id": cfg.DeviceID, "inventory": buildInventory(cfg), - "metrics": buildMetrics(cfg.CheckTargets), + "metrics": buildMetrics(cfg), } var resp heartbeatResponse if err := postJSON(ctx, client, cfg.ServerURL+"/api/agent/heartbeat", cfg.DeviceToken, body, &resp); err != nil { @@ -300,7 +302,8 @@ func (cfg config) displayHostname() string { return hostnameValue() + strings.TrimSpace(cfg.HostnameSuffix) } -func buildMetrics(targets []string) map[string]any { +func buildMetrics(cfg config) map[string]any { + serverTarget := serverCheckTarget(cfg.ServerURL) return map[string]any{ "system": jsonObjectOrEmpty(commandOutput("ubus", "call", "system", "info")), "loadavg": strings.TrimSpace(readFileString("/proc/loadavg")), @@ -308,10 +311,46 @@ func buildMetrics(targets []string) map[string]any { "memory": memoryInfo(), "disk": diskInfo(), "interface_counters": interfaceCounters(), - "connectivity_checks": connectivityChecks(targets), + "connectivity_checks": connectivityChecks(effectiveCheckTargets(cfg.CheckTargets, serverTarget)), + "server_check_target": serverTarget, } } +func effectiveCheckTargets(targets []string, serverTarget string) []string { + result := make([]string, 0, len(targets)+1) + seen := map[string]bool{} + for _, target := range targets { + target = strings.TrimSpace(target) + if target == "" || seen[target] { + continue + } + result = append(result, target) + seen[target] = true + } + if serverTarget != "" && !seen[serverTarget] { + result = append(result, serverTarget) + } + return result +} + +func serverCheckTarget(serverURL string) string { + parsed, err := url.Parse(strings.TrimSpace(serverURL)) + if err != nil { + return "" + } + host := parsed.Hostname() + if host == "" { + host = parsed.Host + } + if host == "" { + return "" + } + if ip := net.ParseIP(host); ip != nil { + return ip.String() + } + return strings.TrimSpace(host) +} + func processCommand(ctx context.Context, client *http.Client, cfg config, cmd command) { output, exitCode := runCommand(ctx, cfg, cmd) status := "completed" diff --git a/agent/go/cmd/rmm-agent/main_test.go b/agent/go/cmd/rmm-agent/main_test.go index 340428f..e9feb87 100644 --- a/agent/go/cmd/rmm-agent/main_test.go +++ b/agent/go/cmd/rmm-agent/main_test.go @@ -78,3 +78,30 @@ rtt min/avg/max/mdev = 10.100/11.200/12.300/0.100 ms` t.Fatalf("expected avg latency 11.2, got %v", latency) } } + +func TestServerCheckTarget(t *testing.T) { + tests := map[string]string{ + "http://10.10.10.10:18082": "10.10.10.10", + "https://rmm.daemonlord.ru": "rmm.daemonlord.ru", + "http://[2001:db8::1]:18082": "2001:db8::1", + "http://127.0.0.1:8080/path": "127.0.0.1", + "not a valid url": "", + } + for input, want := range tests { + if got := serverCheckTarget(input); got != want { + t.Fatalf("serverCheckTarget(%q) = %q, want %q", input, got, want) + } + } +} + +func TestEffectiveCheckTargetsAddsServerTarget(t *testing.T) { + got := effectiveCheckTargets([]string{"1.1.1.1", "10.10.10.10", "1.1.1.1"}, "10.10.10.10") + if len(got) != 2 || got[0] != "1.1.1.1" || got[1] != "10.10.10.10" { + t.Fatalf("unexpected targets: %#v", got) + } + + got = effectiveCheckTargets([]string{"1.1.1.1"}, "10.10.10.10") + if len(got) != 2 || got[1] != "10.10.10.10" { + t.Fatalf("expected server target to be appended, got %#v", got) + } +} diff --git a/agent/openwrt/rmm-agent.sh b/agent/openwrt/rmm-agent.sh index 398b0ba..b9bb7ba 100644 --- a/agent/openwrt/rmm-agent.sh +++ b/agent/openwrt/rmm-agent.sh @@ -338,10 +338,25 @@ NR > 2 { END { printf "]" }' /proc/net/dev 2>/dev/null } +server_check_target() { + printf '%s' "$SERVER_URL" | sed 's#^[^:/]*://##; s#/.*##; s#^\[\([^]]*\)\].*#\1#; s#:[0-9][0-9]*$##' +} + connectivity_checks_json() { printf '[' first=1 - for target in $CHECK_TARGETS; do + server_target="$(server_check_target)" + targets="$CHECK_TARGETS" + if [ -n "$server_target" ]; then + case " $targets " in + *" $server_target "*) + ;; + *) + targets="$targets $server_target" + ;; + esac + fi + for target in $targets; do [ -n "$target" ] || continue output="$(ping -c 3 -W 2 "$target" 2>&1 || true)" loss="$(printf '%s' "$output" | awk -F',' '/packet loss/ { @@ -393,8 +408,9 @@ build_metrics() { disk="$(json_object_or_empty "$(disk_json)")" counters="$(json_array_or_empty "$(interface_counters_json)")" connectivity="$(json_array_or_empty "$(connectivity_checks_json)")" + server_target="$(json_escape "$(server_check_target)")" - printf '{"system":%s,"loadavg":"%s","uptime":"%s","memory":%s,"disk":%s,"interface_counters":%s,"connectivity_checks":%s}' "$info" "$loadavg" "$uptime" "$memory" "$disk" "$counters" "$connectivity" + printf '{"system":%s,"loadavg":"%s","uptime":"%s","memory":%s,"disk":%s,"interface_counters":%s,"connectivity_checks":%s,"server_check_target":"%s"}' "$info" "$loadavg" "$uptime" "$memory" "$disk" "$counters" "$connectivity" "$server_target" } save_config() { diff --git a/agent/package/rmm-agent/files/usr/bin/rmm-agent b/agent/package/rmm-agent/files/usr/bin/rmm-agent index 02f8034..5e25a88 100644 --- a/agent/package/rmm-agent/files/usr/bin/rmm-agent +++ b/agent/package/rmm-agent/files/usr/bin/rmm-agent @@ -338,10 +338,25 @@ NR > 2 { END { printf "]" }' /proc/net/dev 2>/dev/null } +server_check_target() { + printf '%s' "$SERVER_URL" | sed 's#^[^:/]*://##; s#/.*##; s#^\[\([^]]*\)\].*#\1#; s#:[0-9][0-9]*$##' +} + connectivity_checks_json() { printf '[' first=1 - for target in $CHECK_TARGETS; do + server_target="$(server_check_target)" + targets="$CHECK_TARGETS" + if [ -n "$server_target" ]; then + case " $targets " in + *" $server_target "*) + ;; + *) + targets="$targets $server_target" + ;; + esac + fi + for target in $targets; do [ -n "$target" ] || continue output="$(ping -c 3 -W 2 "$target" 2>&1 || true)" loss="$(printf '%s' "$output" | awk -F',' '/packet loss/ { @@ -393,8 +408,9 @@ build_metrics() { disk="$(json_object_or_empty "$(disk_json)")" counters="$(json_array_or_empty "$(interface_counters_json)")" connectivity="$(json_array_or_empty "$(connectivity_checks_json)")" + server_target="$(json_escape "$(server_check_target)")" - printf '{"system":%s,"loadavg":"%s","uptime":"%s","memory":%s,"disk":%s,"interface_counters":%s,"connectivity_checks":%s}' "$info" "$loadavg" "$uptime" "$memory" "$disk" "$counters" "$connectivity" + printf '{"system":%s,"loadavg":"%s","uptime":"%s","memory":%s,"disk":%s,"interface_counters":%s,"connectivity_checks":%s,"server_check_target":"%s"}' "$info" "$loadavg" "$uptime" "$memory" "$disk" "$counters" "$connectivity" "$server_target" } save_config() { diff --git a/web/app.js b/web/app.js index 213cd94..8138cf9 100644 --- a/web/app.js +++ b/web/app.js @@ -558,13 +558,15 @@ function formatConnectivity(checks) { function renderHealthSummary(device) { const checks = Array.isArray(device.metrics && device.metrics.connectivity_checks) ? device.metrics.connectivity_checks : []; - const serverHost = window.location.hostname; + const serverHost = (device.metrics && device.metrics.server_check_target) || window.location.hostname; const serverCheck = checks.find((check) => check.target === serverHost); const wanChecks = checks.filter((check) => check.target !== serverHost); const wanReachable = wanChecks.some((check) => check.reachable); + const serverStatus = serverCheck ? (serverCheck.reachable ? `Есть, ${serverCheck.latency_ms} ms` : "Нет") : (device.online ? "Есть" : "Нет данных"); + const serverState = serverCheck ? (serverCheck.reachable ? "ok" : "bad") : (device.online ? "ok" : "warn"); const rows = [ ["Статус", device.online ? "На связи" : "Нет связи", device.online ? "ok" : "bad"], - ["Связь с сервером", serverCheck ? (serverCheck.reachable ? `Есть, ${serverCheck.latency_ms} ms` : "Нет") : "Нет данных", serverCheck && serverCheck.reachable ? "ok" : "warn"], + ["Связь с сервером", serverStatus, serverState], ["Интернет/WAN", wanChecks.length ? (wanReachable ? "Есть" : "Нет") : "Нет данных", wanChecks.length && !wanReachable ? "bad" : "ok"], ["Активные проблемы", String(device.active_alerts || 0), device.active_alerts ? "warn" : "ok"], ["WAN IP", device.inventory && device.inventory.wan_ip ? device.inventory.wan_ip : "-", "neutral"],