From 6cfb00a847b2deaccdd1830fc7c0c2a92b83cf56 Mon Sep 17 00:00:00 2001 From: protokey Date: Fri, 25 Sep 2026 15:55:19 +0400 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B8=D0=B7=20make=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B8=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 15 +++++- src/serverus/README.md | 10 ++++ src/serverus/__main__.py | 112 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/serverus/__main__.py diff --git a/Makefile b/Makefile index 555567c..40d9c42 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,14 @@ PIP := $(VENV)/bin/pip WHEELS_DIR := wheels REQUIREMENTS := requirements.txt MANAGE := cd src/ && ../$(PYTHON) manage.py +SERVERUS := cd src/ && ../$(PYTHON) -m serverus +# доступы к серверам, настроенным вручную через serverus (папка в .gitignore) +SERVERUS_ACCESS_DIR := $(abspath extra/serverus) MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS)) -.PHONY: install freeze wheels sync sync-offline clean help venv dj_makemigrations dj_migrate dj_superuser dj_run dj_test dj_startapp dj_worker tg_run +.PHONY: install freeze wheels sync sync-offline clean help venv dj_makemigrations dj_migrate dj_superuser dj_run dj_test dj_startapp dj_worker tg_run serverus_xui serverus_release help: @echo "make install [ ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/" @@ -17,6 +20,8 @@ help: @echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения" @echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)" @echo "make clean - удалить $(WHEELS_DIR)/" + @echo "make serverus_xui ip= password= - настроить свежий сервер и поставить 3x-ui, доступы в $(SERVERUS_ACCESS_DIR)//" + @echo "make serverus_release ip= - вернуть сервер, настроенный через serverus_xui: вход по паролю на 22 порт" venv: @test -d $(VENV) || python3 -m venv $(VENV) @@ -75,3 +80,11 @@ dj_startapp: tg_run: $(MANAGE) telegram_run + +serverus_xui: + @test -n "$(ip)" -a -n "$(password)" || { echo "Укажите сервер: make serverus_xui ip= password="; exit 1; } + $(SERVERUS) xui $(ip) '$(password)' --out $(SERVERUS_ACCESS_DIR)/$(ip) + +serverus_release: + @test -n "$(ip)" || { echo "Укажите сервер: make serverus_release ip="; exit 1; } + $(SERVERUS) release --out $(SERVERUS_ACCESS_DIR)/$(ip) diff --git a/src/serverus/README.md b/src/serverus/README.md index b6bfcf9..ccd0ff1 100644 --- a/src/serverus/README.md +++ b/src/serverus/README.md @@ -72,4 +72,14 @@ server.release(generate_password()) Плейбуки лежат в `playbooks/`, общие шаги в `playbooks/tasks/`. Новая операция - это плейбук и метод в `Server` или его наследнике. +## Ручная проверка на живом сервере + +``` +make serverus_xui ip= password= # bootstrap + 3x-ui +make serverus_release ip= # вернуть сервер +``` + +То же без Makefile: `python -m serverus xui --out ` и `python -m serverus release --out `. +Доступы (ключ, пароль root, порт ssh, отпечаток, панель) пишутся в `extra/serverus//` до начала настройки, так что доступ к серверу не теряется, даже если настройка упала. + Тесты: `python -m unittest serverus.tests` из `src/`. Плейбуки проверены на Debian 13 в контейнере с systemd. diff --git a/src/serverus/__main__.py b/src/serverus/__main__.py new file mode 100644 index 0000000..d1290f8 --- /dev/null +++ b/src/serverus/__main__.py @@ -0,0 +1,112 @@ +"""Ручная проверка сценариев на живом сервере. + + python -m serverus xui --out + python -m serverus release --out + +Доступы (ключ, пароль root, порт ssh, отпечаток, панель) пишутся в /access.json до начала настройки: +bootstrap меняет пароль и порт ssh, без этого файла доступ к серверу можно потерять. +""" +import argparse +import json +import sys +from pathlib import Path + +from . import Host, Server, XuiPanel, generate_keypair, generate_password, generate_ssh_port + +ACCESS_FILE = 'access.json' +KEY_FILE = 'id_ed25519' +PANEL_PORT = 12345 + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(prog='python -m serverus') + commands = parser.add_subparsers(dest='command', required=True) + + xui = commands.add_parser('xui', help='bootstrap свежего сервера и установка 3x-ui') + xui.add_argument('address') + xui.add_argument('password', help='текущий пароль root') + xui.add_argument('--out', type=Path, required=True, help='папка для доступов к серверу') + + release = commands.add_parser('release', help='вернуть сервер: вход по паролю на 22 порт, новый пароль root') + release.add_argument('--out', type=Path, required=True, help='папка с доступами от команды xui') + + args = parser.parse_args(argv) + return install_xui(args.address, args.password, args.out) if args.command == 'xui' else release_server(args.out) + + +def install_xui(address: str, password: str, out: Path) -> int: + keys = generate_keypair() + access = { + 'address': address, + 'ssh_port': generate_ssh_port(), + 'root_password': generate_password(), + 'private_key': keys.private_key, + 'known_hosts': '', + 'panel': { + 'username': 'admin', + 'password': generate_password(16), + 'panel_port': PANEL_PORT, + 'web_base_path': generate_password(18), + }, + } + save(out, access) + + result = Server(Host(address, private_key=keys.private_key)).bootstrap( + password, keys.public_key, access['root_password'], access['ssh_port'], + ) + access['known_hosts'] = result.known_hosts + save(out, access) + report('bootstrap', result) + if not result.ok: + return 1 + + host = load_host(access) + result = XuiPanel(host).install(**access['panel']) + report('xui', result) + if not result.ok: + return 1 + + panel = access['panel'] + print(f"uname: {Server(host).facts().data.get('uname', '?')}") + print(f"panel: http(s)://{address}:{panel['panel_port']}/{panel['web_base_path']}/") + print(f"panel login: {panel['username']} / {panel['password']}") + print(f"ssh: ssh -i {out / KEY_FILE} -p {access['ssh_port']} root@{address}") + return 0 + + +def release_server(out: Path) -> int: + access = json.loads((out / ACCESS_FILE).read_text()) + new_password = generate_password() + result = Server(load_host(access)).release(new_password) + report('release', result) + if not result.ok: + return 1 + access.update(ssh_port=22, root_password=new_password, known_hosts='') + save(out, access) + print(f"ssh: ssh -p 22 root@{access['address']}, root password: {new_password}") + return 0 + + +def load_host(access: dict) -> Host: + return Host( + access['address'], port=access['ssh_port'], private_key=access['private_key'], + known_hosts=access['known_hosts'], + ) + + +def save(out: Path, access: dict): + out.mkdir(parents=True, exist_ok=True) + for name, content in ((ACCESS_FILE, json.dumps(access, indent=2)), (KEY_FILE, access['private_key'])): + path = out / name + path.touch(mode=0o600, exist_ok=True) + path.write_text(content) + + +def report(step: str, result): + print(f'{step}: {"ok" if result.ok else "FAILED"}') + if result.error: + print(result.error, file=sys.stderr) + + +if __name__ == '__main__': + sys.exit(main())