# 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()