130 lines
5.7 KiB
Python
130 lines
5.7 KiB
Python
# 2026-03-29 — handler.py: установка nginx на ВМ по SSH.
|
||
# sless_job runtime: python3.11, entrypoint: handler.install
|
||
#
|
||
# event_json: {} (параметров нет — nginx ставится с дефолтной конфигурацией)
|
||
#
|
||
# env_vars:
|
||
# VM_IP: внешний IP ВМ
|
||
# SSH_USER: логин (ubuntu)
|
||
# SSH_KEY: содержимое приватного SSH-ключа (PEM)
|
||
|
||
import os, io, time
|
||
import paramiko
|
||
|
||
|
||
def _load_key(content):
|
||
for cls in (paramiko.Ed25519Key, paramiko.RSAKey, paramiko.ECDSAKey):
|
||
try:
|
||
return cls.from_private_key(io.StringIO(content))
|
||
except Exception:
|
||
pass
|
||
raise ValueError("Неподдерживаемый тип SSH-ключа")
|
||
|
||
|
||
def _ssh_connect(retries=5, delay=10):
|
||
key = _load_key(os.environ["SSH_KEY"])
|
||
client = paramiko.SSHClient()
|
||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||
last_err = None
|
||
for attempt in range(retries):
|
||
try:
|
||
client.connect(
|
||
hostname=os.environ["VM_IP"],
|
||
username=os.environ["SSH_USER"],
|
||
pkey=key,
|
||
timeout=15,
|
||
)
|
||
return client
|
||
except Exception as e:
|
||
last_err = e
|
||
if attempt < retries - 1:
|
||
time.sleep(delay)
|
||
raise RuntimeError(f"SSH не удалось после {retries} попыток: {last_err}")
|
||
|
||
|
||
def _run(client, cmd, timeout=120, check=True):
|
||
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
||
code = stdout.channel.recv_exit_status()
|
||
out = stdout.read().decode(errors="replace").strip()
|
||
err = stderr.read().decode(errors="replace").strip()
|
||
if check and code != 0:
|
||
raise RuntimeError(f"Ошибка (exit {code}):\n{cmd}\nstderr: {err}")
|
||
return code, out, err
|
||
|
||
|
||
def _wait_apt_lock(client, attempts=20, delay=10):
|
||
"""Ждать завершения cloud-init и убить авто-обновления. Ubuntu 22.04+."""
|
||
# Шаг 1: Ждём завершения cloud-init — он держит apt при первом старте VM
|
||
_run(client, "timeout 300 sudo cloud-init status --wait 2>/dev/null; true", check=False, timeout=310)
|
||
# Шаг 2: Mask (не просто disable) — systemd не сможет перезапустить
|
||
_run(client, "sudo systemctl mask unattended-upgrades apt-daily.service apt-daily-upgrade.service apt-daily.timer apt-daily-upgrade.timer 2>/dev/null; true", check=False)
|
||
_run(client, "sudo systemctl stop unattended-upgrades apt-daily.service apt-daily-upgrade.service 2>/dev/null; true", check=False)
|
||
# Шаг 3: Добить оставшиеся apt/dpkg процессы
|
||
_run(client, "sudo pkill -9 -x unattended-upgrades apt-get apt dpkg 2>/dev/null; true", check=False)
|
||
_run(client, "sudo kill -9 $(sudo lsof -t /var/lib/dpkg/lock-frontend 2>/dev/null) 2>/dev/null; true", check=False)
|
||
# Шаг 4: Убрать стейл-локи и починить dpkg
|
||
_run(client, "sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock /var/lib/apt/lists/lock 2>/dev/null; true", check=False)
|
||
_run(client, "sudo dpkg --configure -a 2>/dev/null; true", check=False)
|
||
time.sleep(3)
|
||
|
||
locks = ["/var/lib/dpkg/lock-frontend", "/var/lib/dpkg/lock", "/var/lib/apt/lists/lock"]
|
||
for i in range(attempts):
|
||
all_free = all(
|
||
_run(client, f"sudo flock -n {lock} true 2>/dev/null", check=False)[0] == 0
|
||
for lock in locks
|
||
)
|
||
if all_free:
|
||
return
|
||
_run(client, "sudo pkill -9 -x apt-get apt dpkg 2>/dev/null; true", check=False)
|
||
_run(client, "sudo kill -9 $(sudo lsof -t /var/lib/dpkg/lock-frontend 2>/dev/null) 2>/dev/null; true", check=False)
|
||
if i < attempts - 1:
|
||
time.sleep(delay)
|
||
raise RuntimeError("apt lock занят слишком долго — проверьте процессы на ВМ")
|
||
|
||
|
||
def install(event):
|
||
"""Установить nginx. Если уже установлен — проверить что запущен."""
|
||
client = _ssh_connect()
|
||
try:
|
||
# Проверить: уже установлен?
|
||
code, ver_out, _ = _run(client, "nginx -v 2>&1", check=False)
|
||
already_installed = "nginx version" in ver_out
|
||
|
||
if already_installed:
|
||
# Убедиться что сервис запущен
|
||
_run(client, "sudo systemctl start nginx", check=False)
|
||
version = ver_out.replace("nginx version: nginx/", "").strip()
|
||
_, http_code, _ = _run(
|
||
client, "curl -s -o /dev/null -w '%{http_code}' http://localhost", check=False
|
||
)
|
||
return {
|
||
"status": "already_installed",
|
||
"version": version,
|
||
"http_check": http_code,
|
||
}
|
||
|
||
_wait_apt_lock(client)
|
||
_run(client, "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 update -qq", timeout=420)
|
||
_run(
|
||
client,
|
||
"sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y -qq nginx",
|
||
timeout=300,
|
||
)
|
||
_run(client, "sudo systemctl enable nginx")
|
||
_run(client, "sudo systemctl start nginx")
|
||
|
||
# Проверить HTTP-ответ на localhost
|
||
_, http_code, _ = _run(
|
||
client, "curl -s -o /dev/null -w '%{http_code}' http://localhost", check=False
|
||
)
|
||
_, ver_out, _ = _run(client, "nginx -v 2>&1", check=False)
|
||
version = ver_out.replace("nginx version: nginx/", "").strip()
|
||
|
||
return {
|
||
"status": "ok",
|
||
"version": version,
|
||
"http_check": http_code,
|
||
}
|
||
finally:
|
||
client.close()
|