mirror of
https://github.com/andrey271192/kaskad.git
synced 2026-09-20 13:49:56 +00:00
feat: one-command install/uninstall + fix delete-ams in WebUI
- install.sh: интерактивный one-liner — ставит WG, ключи, скрипты, бот, WebUI, cron; сам поднимает RU-сервер если он чистый - uninstall.sh: полная очистка (сервисы, конфиги, cron, /opt/kaskad) с подтверждением; опц. KASKAD_PURGE_KEYS=1 для затирания ключей - webui: кнопка «удалить» для зарубежных серверов теперь явно красная (class danger.solid), для локального — «⛔ нельзя» вместо disabled - webui: DELETE /api/ams/<id> переписан — портативный python-based peer-removal, явные ошибки, поддержка ?force=1 - README + docs/install.md: переписаны под обычного пользователя, one-command flow на первом плане, ручной — как fallback Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
73
webui/app.py
73
webui/app.py
@@ -395,37 +395,58 @@ systemctl enable wg-quick@ru 2>&1 | tail -1
|
||||
@app.route("/api/ams/<sid>", methods=["DELETE"])
|
||||
@require_auth
|
||||
def api_ams_remove(sid):
|
||||
data = load_data()
|
||||
target = next((a for a in data.get("ams_servers", []) if a["id"] == sid or a["host"] == sid), None)
|
||||
if not target: return jsonify(error="не найден"), 404
|
||||
if target.get("is_local"): return jsonify(error="нельзя удалить local"), 400
|
||||
"""Удалить зарубежный (ам.) сервер из каскада.
|
||||
|
||||
Шаги:
|
||||
1) на каждом RU-сервере убрать [Peer] с pubkey удаляемого ам.
|
||||
2) обновить /etc/wireguard/ru-servers.json и разлить по остальным ам.
|
||||
3) синхронизировать живой wg-интерфейс на RU через `wg set ... peer ... remove`
|
||||
|
||||
?force=1 — продолжить, даже если какой-то RU недоступен.
|
||||
"""
|
||||
force = request.args.get("force") in ("1", "true", "yes")
|
||||
data = load_data()
|
||||
target = next((a for a in data.get("ams_servers", [])
|
||||
if a["id"] == sid or a["host"] == sid), None)
|
||||
if not target:
|
||||
return jsonify(error=f"ам. сервер '{sid}' не найден"), 404
|
||||
if target.get("is_local"):
|
||||
return jsonify(error="нельзя удалить сервер, на котором запущен WebUI"), 400
|
||||
|
||||
pubkey = target["pubkey"]
|
||||
rus = ru_list_data()
|
||||
peer_results = []
|
||||
peer_results, hard_fail = [], []
|
||||
|
||||
for ru in rus:
|
||||
cmd = f"""
|
||||
python3 - <<'PY'
|
||||
import pathlib, re
|
||||
p = pathlib.Path('/etc/wireguard/wg_ru.conf')
|
||||
t = p.read_text()
|
||||
parts = re.split(r'(\\[Peer\\])', t)
|
||||
result = parts[0]
|
||||
i = 1
|
||||
while i < len(parts):
|
||||
block = parts[i] + (parts[i+1] if i+1 < len(parts) else '')
|
||||
if {target['pubkey']!r} in block:
|
||||
i += 2; continue
|
||||
result += block
|
||||
i += 2
|
||||
p.write_text(result)
|
||||
PY
|
||||
wg syncconf wg_ru <(wg-quick strip wg_ru) 2>&1
|
||||
"""
|
||||
cmd = (
|
||||
f"set -e; cd /etc/wireguard; "
|
||||
f"cp -a wg_ru.conf wg_ru.conf.bak.$(date +%s); "
|
||||
f"python3 -c \"import pathlib,re,sys; "
|
||||
f"p=pathlib.Path('wg_ru.conf'); t=p.read_text(); "
|
||||
f"blocks=re.split(r'(?m)(?=^\\[Peer\\])', t); "
|
||||
f"kept=[b for b in blocks if {pubkey!r} not in b]; "
|
||||
f"p.write_text(''.join(kept))\"; "
|
||||
f"wg set wg_ru peer {shlex.quote(pubkey)} remove 2>/dev/null || true; "
|
||||
f"echo OK"
|
||||
)
|
||||
out_pr, rc_pr = ssh_ru(ru, cmd, timeout=20)
|
||||
peer_results.append({"ru": ru["id"], "ok": rc_pr == 0})
|
||||
ok = (rc_pr == 0 and "OK" in (out_pr or ""))
|
||||
peer_results.append({"ru": ru["id"], "ok": ok, "msg": (out_pr or "")[-200:]})
|
||||
if not ok:
|
||||
hard_fail.append(f"{ru['id']}: rc={rc_pr} {(out_pr or '')[-120:]}")
|
||||
|
||||
if hard_fail and not force:
|
||||
return jsonify(
|
||||
error=("не удалось убрать peer на: " + "; ".join(hard_fail) +
|
||||
". Повторите с ?force=1 чтобы удалить запись принудительно."),
|
||||
peers=peer_results,
|
||||
), 502
|
||||
|
||||
data["ams_servers"] = [a for a in data["ams_servers"] if a["id"] != target["id"]]
|
||||
save_and_distribute(data)
|
||||
return jsonify(removed=target["id"], peers=peer_results)
|
||||
ok, err = save_and_distribute(data)
|
||||
if not ok and not force:
|
||||
return jsonify(error=f"sync конфига: {err}", peers=peer_results), 500
|
||||
return jsonify(removed=target["id"], peers=peer_results, force=force)
|
||||
|
||||
|
||||
@app.route("/api/domains", methods=["POST"])
|
||||
|
||||
@@ -51,6 +51,17 @@ button:hover:not(:disabled) { background: #30363d; border-color: #58a6ff; color:
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
button.danger { color: #f85149; border-color: #4a1f23; }
|
||||
button.danger:hover:not(:disabled) { background: #4a1f23; color: #fff; border-color: #f85149; }
|
||||
button.danger.solid {
|
||||
background: #b1361a;
|
||||
color: #fff;
|
||||
border-color: #f85149;
|
||||
font-weight: 600;
|
||||
}
|
||||
button.danger.solid:hover:not(:disabled) {
|
||||
background: #f85149;
|
||||
color: #fff;
|
||||
border-color: #f85149;
|
||||
}
|
||||
button.sm { padding: 2px 8px; font-size: 11px; }
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -79,18 +79,25 @@
|
||||
|
||||
<!-- Ам. серверы -->
|
||||
<section>
|
||||
<h2>Ам. серверы</h2>
|
||||
<h2>Ам. серверы (зарубежные)</h2>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Host</th><th>SSH</th><th>Tunnel</th><th>Pubkey</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<template x-for="a in state.ams_servers" :key="a.id">
|
||||
<tr>
|
||||
<td><strong x-text="a.id"></strong> <span class="muted" x-show="a.is_local">(local)</span></td>
|
||||
<td><strong x-text="a.id"></strong> <span class="muted" x-show="a.is_local">(этот сервер)</span></td>
|
||||
<td x-text="a.host"></td>
|
||||
<td x-text="a.ssh_port"></td>
|
||||
<td x-text="a.tunnel_ip"></td>
|
||||
<td class="muted mono" x-text="a.pubkey.slice(0, 20) + '...'"></td>
|
||||
<td><button class="danger" @click="removeAms(a.id)" :disabled="a.is_local">удалить</button></td>
|
||||
<td>
|
||||
<template x-if="!a.is_local">
|
||||
<button class="danger solid" @click="removeAms(a.id)">удалить</button>
|
||||
</template>
|
||||
<template x-if="a.is_local">
|
||||
<span class="muted" title="нельзя удалить сервер, на котором запущен WebUI/бот">⛔ нельзя</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -258,8 +265,18 @@
|
||||
this.load();
|
||||
},
|
||||
async removeAms(id) {
|
||||
if (!confirm(`Удалить ам. '${id}'? Snimet peer на всех RU.`)) return;
|
||||
await this.req('DELETE',`/api/ams/${id}`); this.flash('удалено'); this.load();
|
||||
if (!confirm(`Удалить зарубежный сервер '${id}'?\n\nЧто произойдёт:\n • peer уберётся со всех RU-серверов\n • запись удалится из конфига\n • сам сервер ${id} останется жив, но больше не будет частью каскада\n\nПродолжить?`)) return;
|
||||
try {
|
||||
const r = await this.req('DELETE',`/api/ams/${id}`);
|
||||
const peers = (r.peers||[]).map(p => `${p.ru}: ${p.ok?'✓':'✗'}`).join(', ');
|
||||
this.flash(`удалён ${id}${peers ? ' ('+peers+')' : ''}`);
|
||||
} catch (e) {
|
||||
if (confirm(`Ошибка: ${e.message}\n\nУдалить запись принудительно (force, даже если RU-серверы недоступны)?`)) {
|
||||
await this.req('DELETE',`/api/ams/${id}?force=1`);
|
||||
this.flash(`удалён ${id} (force)`);
|
||||
}
|
||||
}
|
||||
this.load();
|
||||
},
|
||||
async addDomains() {
|
||||
const doms = this.forms.domains.split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);
|
||||
|
||||
Reference in New Issue
Block a user