Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d1be2ed6 | ||
|
|
af357a0a6f | ||
|
|
86dabfe3a0 | ||
|
|
6fa54041f6 |
@@ -11,4 +11,4 @@
|
|||||||
|
|
||||||
BRAND=Forgetting
|
BRAND=Forgetting
|
||||||
CHANNEL=Beta
|
CHANNEL=Beta
|
||||||
VERSION=0.5.1
|
VERSION=0.5.3
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ TOKEN=$(curl -s -X POST http://HOST:PORT/login \
|
|||||||
|
|
||||||
| Метод | Ответ |
|
| Метод | Ответ |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.5.1" }` — имя и версия панели для вордмарка; за авторизацией, чтобы сканер не опознал панель |
|
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.5.3" }` — имя и версия панели для вордмарка; за авторизацией, чтобы сканер не опознал панель |
|
||||||
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
||||||
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-agent",
|
"name": "awg-agent",
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl",
|
"description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-control",
|
"name": "awg-control",
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const now = () => Math.floor(Date.now() / 1000);
|
|||||||
const rnd = (n: number) => Math.floor(Math.random() * n);
|
const rnd = (n: number) => Math.floor(Math.random() * n);
|
||||||
const b64 = (n: number) => crypto.randomBytes(n).toString("base64");
|
const b64 = (n: number) => crypto.randomBytes(n).toString("base64");
|
||||||
const GEN = "3.1";
|
const GEN = "3.1";
|
||||||
const MOCK_VERSION = "0.5.1";
|
const MOCK_VERSION = "0.5.3";
|
||||||
|
|
||||||
// ── пользователи (vpn://) ──
|
// ── пользователи (vpn://) ──
|
||||||
interface U { name: string; ip: string; pub_key: string; vpn_key: string; key_gen: string; online: boolean; lastHandshake: number; rx: number; tx: number }
|
interface U { name: string; ip: string; pub_key: string; vpn_key: string; key_gen: string; online: boolean; lastHandshake: number; rx: number; tx: number }
|
||||||
@@ -40,7 +40,7 @@ const mnodes: MNode[] = [
|
|||||||
{ id: 1, name: "Frankfurt", ip: "198.51.100.4", gen: "2", online: true, pending: false, version: "0.5.0" },
|
{ id: 1, name: "Frankfurt", ip: "198.51.100.4", gen: "2", online: true, pending: false, version: "0.5.0" },
|
||||||
{ id: 2, name: "Tokyo", ip: "", gen: "2", online: false, pending: true, version: null },
|
{ id: 2, name: "Tokyo", ip: "", gen: "2", online: false, pending: true, version: null },
|
||||||
{ id: 3, name: "Amsterdam", ip: "192.0.2.10", gen: "3.1", online: false, pending: false, version: "0.4.5" }, // старше 0.5.0: хаб не подключает
|
{ id: 3, name: "Amsterdam", ip: "192.0.2.10", gen: "3.1", online: false, pending: false, version: "0.4.5" }, // старше 0.5.0: хаб не подключает
|
||||||
{ id: 4, name: "Helsinki", ip: "192.0.2.44", gen: "3.1", online: true, pending: false, version: "0.5.1" }, // новее панели — жёлтый чип
|
{ id: 4, name: "Helsinki", ip: "192.0.2.44", gen: "3.1", online: true, pending: false, version: "0.5.4" }, // новее панели — жёлтый чип
|
||||||
];
|
];
|
||||||
let nodeId = 3;
|
let nodeId = 3;
|
||||||
const joinStr = (id: number) => "awgjoin://" + Buffer.from(JSON.stringify({ v: 1, h: "localhost", p: 8443, pin: crypto.randomBytes(32).toString("base64url"), n: id, s: crypto.randomBytes(16).toString("base64url") })).toString("base64url");
|
const joinStr = (id: number) => "awgjoin://" + Buffer.from(JSON.stringify({ v: 1, h: "localhost", p: 8443, pin: crypto.randomBytes(32).toString("base64url"), n: id, s: crypto.randomBytes(16).toString("base64url") })).toString("base64url");
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-ui",
|
"name": "awg-ui",
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ export interface MonitorStatus {
|
|||||||
ready: boolean; can_install: boolean; command: string;
|
ready: boolean; can_install: boolean; command: string;
|
||||||
host: { id: number; name: string } | null; // Prometheus на ноде (или будет поставлен туда); null — на этом сервере
|
host: { id: number; name: string } | null; // Prometheus на ноде (или будет поставлен туда); null — на этом сервере
|
||||||
job: { action: 'install' | 'remove'; running: boolean; exit: number | null; log: string } | null; // последняя операция
|
job: { action: 'install' | 'remove'; running: boolean; exit: number | null; log: string } | null; // последняя операция
|
||||||
|
on_node?: boolean; // панель на ноде другой панели: Prometheus этой машины ставит и читает та
|
||||||
}
|
}
|
||||||
|
|
||||||
// Прогресс операции из строк «N/M шаг», которые печатает сам установщик (cli/src/monitor.ts). Шаг N считаем
|
// Прогресс операции из строк «N/M шаг», которые печатает сам установщик (cli/src/monitor.ts). Шаг N считаем
|
||||||
// выполненным наполовину: так полоска не стоит на нуле, пока идёт долгий apt, и не обещает больше, чем известно.
|
// выполненным наполовину: так полоска не стоит на нуле, пока идёт долгий apt, и не обещает больше, чем известно.
|
||||||
const STEP = /^(\d+)\/(\d+)\s+(.+)$/;
|
const STEP = /^(\d+)\/(\d+)\s+(.+)$/;
|
||||||
|
const SETTLE_MS = 90_000;
|
||||||
function JobProgress({ job, starting }: { job: MonitorStatus['job']; starting: boolean }) {
|
function JobProgress({ job, starting }: { job: MonitorStatus['job']; starting: boolean }) {
|
||||||
let step: [number, number, string] | null = null;
|
let step: [number, number, string] | null = null;
|
||||||
for (const l of job?.log.split('\n') ?? []) { const m = STEP.exec(l.trim()); if (m) step = [Number(m[1]), Number(m[2]), m[3]]; }
|
for (const l of job?.log.split('\n') ?? []) { const m = STEP.exec(l.trim()); if (m) step = [Number(m[1]), Number(m[2]), m[3]]; }
|
||||||
@@ -55,15 +57,22 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
|
|||||||
|
|
||||||
useEffect(() => { void loadStatus(); }, [loadStatus]);
|
useEffect(() => { void loadStatus(); }, [loadStatus]);
|
||||||
|
|
||||||
// Пока идёт установка или удаление — следим за ходом: раз в 2 с читаем статус и лог
|
// Пока идёт установка или удаление — следим за ходом: раз в 2 с читаем статус и лог. Операция
|
||||||
|
// закончилась, а панель ещё не видит результата (на ноде агент перезапускается и переподключается) —
|
||||||
|
// следим дальше, пока ready не совпадёт с ней, но не дольше SETTLE_MS: иначе «Готово» висит рядом
|
||||||
|
// с «не подключён», а пункт «Монитор» ждёт минутного опроса App.
|
||||||
|
const [settle, setSettle] = useState<number | null>(null); // до какого момента ждём ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pending && !status?.job?.running) return;
|
if (!pending && !status?.job?.running && settle === null) return;
|
||||||
const t = setInterval(async () => {
|
const t = setInterval(async () => {
|
||||||
const s = await loadStatus();
|
const s = await loadStatus();
|
||||||
if (s && !s.job?.running) setPending(null);
|
if (!s?.job || s.job.running) return; // агент ноды переподключается — job пока не виден
|
||||||
|
setPending(null);
|
||||||
|
const waiting = s.job.exit === 0 && s.ready !== (s.job.action === 'install');
|
||||||
|
setSettle(d => waiting && (d === null || Date.now() < d) ? d ?? Date.now() + SETTLE_MS : null);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
}, [pending, status?.job?.running, loadStatus]);
|
}, [pending, status?.job?.running, settle, loadStatus]);
|
||||||
|
|
||||||
const run = async (action: 'install' | 'remove') => {
|
const run = async (action: 'install' | 'remove') => {
|
||||||
if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 31 дня) сотрутся. Панель продолжит отдавать /metrics.')) return;
|
if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 31 дня) сотрутся. Панель продолжит отдавать /metrics.')) return;
|
||||||
@@ -73,9 +82,19 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!status) return <section className="st-section"><p className="st-hint">Загрузка…</p></section>;
|
if (!status) return <section className="st-section"><p className="st-hint">Загрузка…</p></section>;
|
||||||
|
if (status.on_node) return (
|
||||||
|
<section className="st-section">
|
||||||
|
<div className="st-row">
|
||||||
|
<span className="st-row-text">
|
||||||
|
<span className="st-row-name">Встроенный Prometheus</span>
|
||||||
|
<span className="st-hint">Этот сервер подключён нодой к другой панели. Prometheus на нём собирает её метрики, поэтому ставится, удаляется и смотрится («Монитор») из неё, а не отсюда.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
const job = status.job;
|
const job = status.job;
|
||||||
const working = !!pending || !!job?.running;
|
const working = !!pending || !!job?.running || settle !== null;
|
||||||
const doing = pending ?? (job?.running ? job.action : null);
|
const doing = pending ?? (job?.running ? job.action : null);
|
||||||
const showJob = !!pending || (!!job && (job.running || job.exit !== null));
|
const showJob = !!pending || (!!job && (job.running || job.exit !== null));
|
||||||
const failed = !!job && job.exit !== null && job.exit !== 0;
|
const failed = !!job && job.exit !== null && job.exit !== 0;
|
||||||
@@ -96,7 +115,7 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="st-row-ctl">
|
<span className="st-row-ctl">
|
||||||
<span className={`chip chip--${status.ready ? 'online' : 'offline'}`}>
|
<span className={`chip chip--${status.ready ? 'online' : 'offline'}`}>
|
||||||
<span className="chip-dot" />{doing === 'install' ? 'ставится' : doing === 'remove' ? 'удаляется' : status.ready ? 'работает' : 'не подключён'}
|
<span className="chip-dot" />{doing === 'install' ? 'ставится' : doing === 'remove' ? 'удаляется' : settle !== null ? (status.ready ? 'отключается' : 'подключается') : status.ready ? 'работает' : 'не подключён'}
|
||||||
</span>
|
</span>
|
||||||
{status.can_install && (status.ready ? (
|
{status.can_install && (status.ready ? (
|
||||||
<button className="btn btn--danger" onClick={() => run('remove')} disabled={working}>
|
<button className="btn btn--danger" onClick={() => run('remove')} disabled={working}>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-cli",
|
"name": "awg-cli",
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
|
|||||||
+1
-1
@@ -188,7 +188,7 @@ trap 'exec >&- 2>&-; wait "$LOG_PID" 2>/dev/null || true; exec 3>&-
|
|||||||
# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает
|
# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает
|
||||||
# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может.
|
# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может.
|
||||||
# При бампе версии правь оба места.
|
# При бампе версии правь оба места.
|
||||||
VERSION="0.5.1"
|
VERSION="0.5.3"
|
||||||
CHANNEL="${CHANNEL:-Beta}"
|
CHANNEL="${CHANNEL:-Beta}"
|
||||||
|
|
||||||
# Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник
|
# Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник
|
||||||
|
|||||||
@@ -177,9 +177,10 @@ func (c *CLI) monitorInstall() {
|
|||||||
if !reDebian.MatchString(osFamily()) {
|
if !reDebian.MatchString(osFamily()) {
|
||||||
mfail("Автоустановка поддерживает только Debian и Ubuntu. На другом дистрибутиве поставьте Prometheus сами и подключите его кнопкой в панели (токен метрик).")
|
mfail("Автоустановка поддерживает только Debian и Ubuntu. На другом дистрибутиве поставьте Prometheus сами и подключите его кнопкой в панели (токен метрик).")
|
||||||
}
|
}
|
||||||
// Нода без своей панели (в том числе сервер, с которого панель переехала): метрики панели
|
// Нода: метрики панели Prometheus получает от агента, а тот — от core. Так и у ноды со своей
|
||||||
// Prometheus получает от агента, а тот — от core.
|
// панелью: Prometheus на машине один, и нужен он core — иначе core его не видит (агент
|
||||||
viaAgent := c.isNode && !contains(c.all, "ui")
|
// отдаёт /prom/… только при AGENT_METRICS_PORT), а панель ноды обходится без «Монитора».
|
||||||
|
viaAgent := c.isNode
|
||||||
target, newPort := os.Getenv("UI_PORT"), false
|
target, newPort := os.Getenv("UI_PORT"), false
|
||||||
if viaAgent {
|
if viaAgent {
|
||||||
if target = os.Getenv("AGENT_METRICS_PORT"); target == "" {
|
if target = os.Getenv("AGENT_METRICS_PORT"); target == "" {
|
||||||
|
|||||||
+13
-3
@@ -15,6 +15,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -81,6 +82,7 @@ type Monitor struct {
|
|||||||
a *App
|
a *App
|
||||||
promURL string
|
promURL string
|
||||||
localEnabled bool
|
localEnabled bool
|
||||||
|
onNode bool // панель на ноде другой панели: Prometheus этой машины собирает метрики core, не наши
|
||||||
http *http.Client
|
http *http.Client
|
||||||
|
|
||||||
srcMu sync.Mutex
|
srcMu sync.Mutex
|
||||||
@@ -102,6 +104,7 @@ func newMonitor(a *App) *Monitor {
|
|||||||
return &Monitor{
|
return &Monitor{
|
||||||
a: a, promURL: envOr("PROM_URL", "http://127.0.0.1:9090"),
|
a: a, promURL: envOr("PROM_URL", "http://127.0.0.1:9090"),
|
||||||
localEnabled: strings.ToLower(envOr("LOCAL_NODE", "on")) != "off",
|
localEnabled: strings.ToLower(envOr("LOCAL_NODE", "on")) != "off",
|
||||||
|
onNode: os.Getenv("CORE_HOST") != "",
|
||||||
http: &http.Client{Transport: &http.Transport{Proxy: nil}},
|
http: &http.Client{Transport: &http.Transport{Proxy: nil}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ func (m *Monitor) forgetSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monitor) findSource() *promSource {
|
func (m *Monitor) findSource() *promSource {
|
||||||
if m.promReady() {
|
if !m.onNode && m.promReady() {
|
||||||
return &promSource{local: true}
|
return &promSource{local: true}
|
||||||
}
|
}
|
||||||
ids := m.a.nodes.IDs()
|
ids := m.a.nodes.IDs()
|
||||||
@@ -316,6 +319,8 @@ func (m *Monitor) target(src *promSource, qs url.Values) int {
|
|||||||
switch {
|
switch {
|
||||||
case src != nil && !src.local:
|
case src != nil && !src.local:
|
||||||
return src.node
|
return src.node
|
||||||
|
case m.onNode:
|
||||||
|
return -1 // ставит его core: кнопка здесь поставила бы Prometheus для core
|
||||||
case m.localEnabled:
|
case m.localEnabled:
|
||||||
return 0
|
return 0
|
||||||
case src != nil:
|
case src != nil:
|
||||||
@@ -364,15 +369,20 @@ func (m *Monitor) router() *Router {
|
|||||||
rt := newRouter()
|
rt := newRouter()
|
||||||
rt.Get("/", func(c *Ctx) bool {
|
rt.Get("/", func(c *Ctx) bool {
|
||||||
src := m.source()
|
src := m.source()
|
||||||
ready := src != nil
|
|
||||||
target := m.target(src, c.R.URL.Query())
|
target := m.target(src, c.R.URL.Query())
|
||||||
job := m.jobState(target)
|
job := m.jobState(target)
|
||||||
|
// Установщик выходит, только когда Prometheus ответил, но проверка выше могла пройти
|
||||||
|
// до этого: тогда страница получила бы «готово» вместе с ready=false и перестала следить.
|
||||||
|
if src == nil && !job.running && job.action == "install" && job.exit != nil && jsToNumberAny(job.exit) == 0 {
|
||||||
|
src = m.source()
|
||||||
|
}
|
||||||
|
ready := src != nil
|
||||||
var jobOut any
|
var jobOut any
|
||||||
if job.can && truthy(job.action) {
|
if job.can && truthy(job.action) {
|
||||||
jobOut = jsjson.Object{{Key: "action", Val: job.action}, {Key: "running", Val: job.running}, {Key: "exit", Val: job.exit}, {Key: "log", Val: job.log}}
|
jobOut = jsjson.Object{{Key: "action", Val: job.action}, {Key: "running", Val: job.running}, {Key: "exit", Val: job.exit}, {Key: "log", Val: job.log}}
|
||||||
}
|
}
|
||||||
return c.JSON(200, jsjson.Object{{Key: "ready", Val: ready}, {Key: "can_install", Val: job.can}, {Key: "job", Val: jobOut},
|
return c.JSON(200, jsjson.Object{{Key: "ready", Val: ready}, {Key: "can_install", Val: job.can}, {Key: "job", Val: jobOut},
|
||||||
{Key: "command", Val: "awg-ctrl monitor install"}, {Key: "host", Val: m.host(target)}})
|
{Key: "command", Val: "awg-ctrl monitor install"}, {Key: "host", Val: m.host(target)}, {Key: "on_node", Val: m.onNode}})
|
||||||
})
|
})
|
||||||
// Установка и удаление — одно и то же: просим awg-ctrl сервера, он запускает CLI от root.
|
// Установка и удаление — одно и то же: просим awg-ctrl сервера, он запускает CLI от root.
|
||||||
ask := func(action string) handler {
|
ask := func(action string) handler {
|
||||||
|
|||||||
Reference in New Issue
Block a user