Prometheus on a node; panel under a URL prefix; open ports on migrate import

A panel without VPN (e.g. after the move) can use a Prometheus that lives on
a node, such as the one with history left on the old server (node #0). Both
directions go through the agent's WSS: the node's Prometheus scrapes
127.0.0.1:AGENT_METRICS_PORT/metrics, the agent checks its token and asks
core (metrics → metrics_res, the same exposition as /metrics, at most once
per 5 s per connection); the monitor tab reads it back through the agent,
which forwards only GET /-/ready and /api/v1/query[_range]. The panel finds
it by itself (local first, then the lowest-id node), and the install/remove
buttons work on a panel without VPN. `monitor install` on a node without its
own panel picks AGENT_METRICS_PORT, writes it to cli.env and restarts the
agent; on the old server it keeps the collected data.

The panel works behind nginx in a subfolder: relative asset and API paths,
relative directory redirects, and the client address from X-Forwarded-For /
X-Real-IP only from trusted proxies (loopback, TRUST_PROXY).

migrate import opens the moved SUB_PORT/NODE_PORT in UFW when it is active.
This commit is contained in:
2026-10-21 19:25:54 +03:00
parent 7ee126af30
commit 96c21408e6
20 changed files with 980 additions and 65 deletions
+41
View File
@@ -0,0 +1,41 @@
package migrate
import (
"os/exec"
"strconv"
"strings"
)
// ufw — вызов ufw (в тестах подменяется). Нет ufw — ошибка, и правила не трогаются.
var ufw = func(args ...string) ([]byte, error) {
if _, err := exec.LookPath("ufw"); err != nil {
return nil, err
}
return exec.Command("ufw", args...).CombinedOutput()
}
// OpenFirewall — открыть в UFW порты, на которые панель переезжает при приёме: подписка и хаб
// берут порты старого сервера, а install.sh открыл только те, что были при установке. Без этого
// нода #0 не дозвонится до хаба, а ретранслятор — до подписки. UFW не установлен или не активен —
// ничего не делаем (как install.sh). opened — что открыто, failed — что не удалось.
func OpenFirewall(ports ...int) (opened, failed []string) {
out, err := ufw("status")
if err != nil || !strings.Contains(strings.ToLower(string(out)), "status: active") {
return nil, nil
}
for _, p := range ports {
if p <= 0 {
continue
}
rule := strconv.Itoa(p) + "/tcp"
if _, err := ufw("allow", rule); err != nil {
failed = append(failed, rule)
} else {
opened = append(opened, rule)
}
}
if len(opened) > 0 {
ufw("reload")
}
return opened, failed
}
+29
View File
@@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"os"
"path/filepath"
"regexp"
@@ -174,3 +175,31 @@ func TestCodeAndSeal(t *testing.T) {
t.Error("открылся повреждённый")
}
}
// Приём переноса открывает новые порты в UFW, только если он активен, — как install.sh.
func TestOpenFirewall(t *testing.T) {
var calls []string
status := "Status: active"
defer func(f func(...string) ([]byte, error)) { ufw = f }(ufw)
ufw = func(args ...string) ([]byte, error) {
calls = append(calls, strings.Join(args, " "))
if args[0] == "status" {
return []byte(status), nil
}
if len(args) > 1 && args[1] == "45000/tcp" {
return nil, errors.New("exit 1")
}
return nil, nil
}
opened, failed := OpenFirewall(40222, 45000, 0)
if strings.Join(opened, ",") != "40222/tcp" || strings.Join(failed, ",") != "45000/tcp" {
t.Errorf("opened %v failed %v", opened, failed)
}
if strings.Join(calls, "; ") != "status; allow 40222/tcp; allow 45000/tcp; reload" {
t.Errorf("вызовы: %v", calls)
}
calls, status = nil, "Status: inactive"
if opened, _ := OpenFirewall(40222); opened != nil || len(calls) != 1 {
t.Errorf("UFW выключен, а правила трогаются: %v", calls)
}
}