chore: можно из make устанавливать сервера

This commit is contained in:
protokey committed 2026-09-25 15:55:19 +04:00
1 parent c4300066f5
commit 6cfb00a847
3 files changed
+136 -1

No files matched your search

+14 -1
View File
@@ -4,11 +4,14 @@ PIP := $(VENV)/bin/pip
WHEELS_DIR := wheels WHEELS_DIR := wheels
REQUIREMENTS := requirements.txt REQUIREMENTS := requirements.txt
MANAGE := cd src/ && ../$(PYTHON) manage.py 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 MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help
PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS)) 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: help:
@echo "make install <pkg> [<pkg> ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/" @echo "make install <pkg> [<pkg> ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/"
@@ -17,6 +20,8 @@ help:
@echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения" @echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения"
@echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)" @echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)"
@echo "make clean - удалить $(WHEELS_DIR)/" @echo "make clean - удалить $(WHEELS_DIR)/"
@echo "make serverus_xui ip=<ip> password=<root_password> - настроить свежий сервер и поставить 3x-ui, доступы в $(SERVERUS_ACCESS_DIR)/<ip>/"
@echo "make serverus_release ip=<ip> - вернуть сервер, настроенный через serverus_xui: вход по паролю на 22 порт"
venv: venv:
@test -d $(VENV) || python3 -m venv $(VENV) @test -d $(VENV) || python3 -m venv $(VENV)
@@ -75,3 +80,11 @@ dj_startapp:
tg_run: tg_run:
$(MANAGE) telegram_run $(MANAGE) telegram_run
serverus_xui:
@test -n "$(ip)" -a -n "$(password)" || { echo "Укажите сервер: make serverus_xui ip=<ip> password=<root_password>"; exit 1; }
$(SERVERUS) xui $(ip) '$(password)' --out $(SERVERUS_ACCESS_DIR)/$(ip)
serverus_release:
@test -n "$(ip)" || { echo "Укажите сервер: make serverus_release ip=<ip>"; exit 1; }
$(SERVERUS) release --out $(SERVERUS_ACCESS_DIR)/$(ip)
+10
View File
@@ -72,4 +72,14 @@ server.release(generate_password())
Плейбуки лежат в `playbooks/`, общие шаги в `playbooks/tasks/`. Новая операция - это плейбук и метод в `Server` или его наследнике. Плейбуки лежат в `playbooks/`, общие шаги в `playbooks/tasks/`. Новая операция - это плейбук и метод в `Server` или его наследнике.
## Ручная проверка на живом сервере
```
make serverus_xui ip=<ip> password=<root_password> # bootstrap + 3x-ui
make serverus_release ip=<ip> # вернуть сервер
```
То же без Makefile: `python -m serverus xui <ip> <password> --out <dir>` и `python -m serverus release --out <dir>`.
Доступы (ключ, пароль root, порт ssh, отпечаток, панель) пишутся в `extra/serverus/<ip>/` до начала настройки, так что доступ к серверу не теряется, даже если настройка упала.
Тесты: `python -m unittest serverus.tests` из `src/`. Плейбуки проверены на Debian 13 в контейнере с systemd. Тесты: `python -m unittest serverus.tests` из `src/`. Плейбуки проверены на Debian 13 в контейнере с systemd.
+112
View File
@@ -0,0 +1,112 @@
"""Ручная проверка сценариев на живом сервере.
python -m serverus xui <ip> <root_password> --out <dir>
python -m serverus release --out <dir>
Доступы (ключ, пароль root, порт ssh, отпечаток, панель) пишутся в <dir>/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())