awg-ctrl: update, reinstall and uninstall from the menu
The awg-ctrl menu only started, stopped and changed the panel login; updating or removing an installation meant finding and running install.sh by hand. The menu gets "Обновить", "Переустановить" and "Удалить" (7–9), and the same as `awg-ctrl update | reinstall | uninstall`. The work stays in install.sh, which now takes ACTION=update|reinstall|uninstall to skip its own menu. The CLI fetches it — the latest from INSTALLER_URL (GitHub by default) for update and reinstall, the copy shipped in the release archive for uninstall, so removal works offline — writes it to a temp file and execs bash on it in the same terminal. It must not run from /opt/awg-control or via `bash -c`: kill_related kills every process whose command line holds the install path, and would kill the installer itself. The CLI passes its original environment, not the one with cli.env loaded, or the installer would take UI_PORT, SERVER_NAME and the rest as explicit overrides. The installer deletes its temp copy on start. The release archive now includes install.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
+5
-2
@@ -638,10 +638,10 @@ func (c *CLI) confirmYes(q string) bool {
|
||||
|
||||
// ── Точка входа ─────────────────────────────────────────────────────────────
|
||||
|
||||
const usage = "Usage: cli <start|stop|restart|status|credentials [user|pass]|join <awgjoin://…> [--no-panel]|unjoin|rehome <хост[:порт]>|peers [drop <область>]|monitor <install|status|remove>|migrate <export [файл]|import <файл>|cancel>>"
|
||||
const usage = "Usage: cli <start|stop|restart|status|credentials [user|pass]|join <awgjoin://…> [--no-panel]|unjoin|rehome <хост[:порт]>|peers [drop <область>]|monitor <install|status|remove>|migrate <export [файл]|import <файл>|cancel>|update|reinstall|uninstall>"
|
||||
|
||||
// Commands — команды оператора; main передаёт сюда всё, что не роль сервиса.
|
||||
var Commands = []string{"start", "stop", "restart", "status", "credentials", "join", "unjoin", "rehome", "peers", "monitor", "migrate"}
|
||||
var Commands = []string{"start", "stop", "restart", "status", "credentials", "join", "unjoin", "rehome", "peers", "monitor", "migrate", "update", "reinstall", "uninstall"}
|
||||
|
||||
// Main выполняет команду CLI (args — без имени бинарника). Пусто — интерактивное меню.
|
||||
func Main(args []string) {
|
||||
@@ -686,6 +686,9 @@ func Main(args []string) {
|
||||
c.peersCmd(sub, hasSub, a2, hasA2)
|
||||
case "monitor":
|
||||
c.monitorCmd(sub)
|
||||
case "update", "reinstall", "uninstall":
|
||||
// Из командной строки — без своего вопроса: установщик сам спросит, что нужно.
|
||||
c.runInstaller(args[0], false)
|
||||
default:
|
||||
fmt.Println(usage)
|
||||
os.Exit(1)
|
||||
|
||||
+11
-1
@@ -491,9 +491,13 @@ func (c *CLI) interactiveMenu() {
|
||||
%s Сменить логин UI
|
||||
%s Сменить пароль UI
|
||||
%s
|
||||
%s Обновить
|
||||
%s Переустановить
|
||||
%s Удалить
|
||||
%s
|
||||
%s Выход
|
||||
|
||||
`, bold("1"), bold("2"), bold("3"), bold("4"), sep, bold("5"), bold("6"), sep, bold("0"))
|
||||
`, bold("1"), bold("2"), bold("3"), bold("4"), sep, bold("5"), bold("6"), sep, bold("7"), bold("8"), bold("9"), sep, bold("0"))
|
||||
}
|
||||
for {
|
||||
printMenu()
|
||||
@@ -518,6 +522,12 @@ func (c *CLI) interactiveMenu() {
|
||||
c.setCredentials("user")
|
||||
case "6":
|
||||
c.setCredentials("pass")
|
||||
case "7":
|
||||
c.runInstaller("update", true)
|
||||
case "8":
|
||||
c.runInstaller("reinstall", true)
|
||||
case "9":
|
||||
c.runInstaller("uninstall", true)
|
||||
case "0":
|
||||
os.Exit(0)
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package cli
|
||||
|
||||
// Обновить, переустановить, удалить — из меню awg-ctrl и командами `awg-ctrl update|reinstall|
|
||||
// uninstall`. Работу делает install.sh (ACTION=… — без его меню): CLI находит скрипт, кладёт во
|
||||
// временный файл и передаёт ему управление (exec), чтобы диалог шёл в том же терминале.
|
||||
//
|
||||
// Скрипт — только из /tmp: install.sh перед заменой файлов убивает процессы, в командной строке
|
||||
// которых есть путь установки (kill_related). Запущенный из /opt/awg-control/install.sh — или
|
||||
// текстом через `bash -c` — он убил бы сам себя.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Откуда брать свежий установщик; INSTALLER_URL в cli.env — своё зеркало.
|
||||
const defaultInstallerURL = "https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh"
|
||||
|
||||
var installerActions = map[string]struct{ title, what string }{
|
||||
"update": {"Обновить", "новая версия; настройки, пользователи и роль сохранятся"},
|
||||
"reinstall": {"Переустановить", "с нуля; данные можно будет сохранить"},
|
||||
"uninstall": {"Удалить", "полностью: сервисы, данные, правила фаервола; копию данных предложит сохранить"},
|
||||
}
|
||||
|
||||
// fetchInstaller — текст install.sh: для обновления и переустановки — свежий из сети (иначе
|
||||
// поставилась бы та же версия), для удаления — копия из установки (работает и без сети).
|
||||
// Копия — запасной путь и для переустановки.
|
||||
func (c *CLI) fetchInstaller(action string) ([]byte, string, error) {
|
||||
local := filepath.Join(c.root, "install.sh")
|
||||
readLocal := func() ([]byte, string, error) {
|
||||
b, err := os.ReadFile(local)
|
||||
return b, local, err
|
||||
}
|
||||
if action == "uninstall" {
|
||||
if b, src, err := readLocal(); err == nil {
|
||||
return b, src, nil
|
||||
}
|
||||
}
|
||||
url := env("INSTALLER_URL", defaultInstallerURL)
|
||||
cl := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := cl.Get(url)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if err == nil && strings.HasPrefix(string(b), "#!") {
|
||||
return b, url, nil
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("%s: HTTP %d", url, resp.StatusCode)
|
||||
}
|
||||
if action == "reinstall" {
|
||||
if b, src, lerr := readLocal(); lerr == nil {
|
||||
fmt.Printf(" %s\n", dim("Установщик не скачался ("+err.Error()+") — берём копию из установки, версия та же"))
|
||||
return b, src, nil
|
||||
}
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// runInstaller — подтверждение и exec install.sh с ACTION. Возвращается только при отказе или
|
||||
// ошибке: после exec процесс CLI заменён установщиком.
|
||||
func (c *CLI) runInstaller(action string, confirm bool) {
|
||||
a, ok := installerActions[action]
|
||||
if !ok {
|
||||
errorf(" Неизвестное действие: %s", action)
|
||||
return
|
||||
}
|
||||
if os.Geteuid() != 0 {
|
||||
errorf(" Нужен root: sudo awg-ctrl %s", action)
|
||||
return
|
||||
}
|
||||
if confirm && !c.confirmYes(fmt.Sprintf(" %s — %s. Продолжить?", a.title, a.what)) {
|
||||
fmt.Println(" Отменено")
|
||||
return
|
||||
}
|
||||
script, src, err := c.fetchInstaller(action)
|
||||
if err != nil {
|
||||
errorf(" Не удалось получить установщик: %v", err)
|
||||
errorf(" %s", dim("Запустите его вручную (README, «Установка») или задайте INSTALLER_URL в "+c.envFile))
|
||||
return
|
||||
}
|
||||
f, err := os.CreateTemp("", "awg-install-*.sh")
|
||||
if err != nil {
|
||||
errorf(" %v", err)
|
||||
return
|
||||
}
|
||||
f.Write(script)
|
||||
f.Close()
|
||||
fmt.Printf(" %s\n\n", dim("Установщик: "+src))
|
||||
bash := "/bin/bash"
|
||||
if p, err := exec.LookPath("bash"); err == nil {
|
||||
bash = p
|
||||
}
|
||||
// Исходное окружение, без cli.env: иначе UI_PORT, SERVER_NAME и прочие сохранённые значения
|
||||
// установщик принял бы за явно заданные параметры.
|
||||
environ := append(withoutKey(c.origEnv, "ACTION"), "ACTION="+action)
|
||||
if err := syscall.Exec(bash, []string{"bash", f.Name()}, environ); err != nil {
|
||||
errorf(" Не удалось запустить установщик: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func withoutKey(environ []string, key string) []string {
|
||||
out := make([]string, 0, len(environ)+1)
|
||||
for _, kv := range environ {
|
||||
if !strings.HasPrefix(kv, key+"=") {
|
||||
out = append(out, kv)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user