#!/usr/bin/env python3 """ deploy_tower.py — nasazení webinar-watcheru na tower (Unraid, python-runner). Heslo se NIKDY neukládá do souboru — bere se z proměnné prostředí TOWER_PW: TOWER_PW=... python deploy_tower.py recon TOWER_PW=... python deploy_tower.py deploy TOWER_PW=... python deploy_tower.py schedule TOWER_PW=... python deploy_tower.py smoke # rychlý test (neblokuje na Telegramu) Vzor převzat z EmailAgent / MedicusFirebird: - skripty v /mnt/user/Scripts// → v kontejneru /scripts// - spouští se: docker exec python-runner python3 /scripts/Webinare/watcher.py - plánování přes Unraid User Scripts plugin (wrapper + schedule.json cron) """ import os import sys import json import posixpath import paramiko for _s in (sys.stdout, sys.stderr): try: _s.reconfigure(encoding="utf-8", errors="replace") except Exception: pass HOST = "192.168.1.76" USER = "root" CONTAINER = "python-runner" LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) HOST_DIR = "/mnt/user/Scripts/Webinare" # na hostiteli (Unraid) CONT_DIR = "/scripts/Webinare" # uvnitř kontejneru PLUGIN_DIR = "/boot/config/plugins/user.scripts" USERSCRIPTS = PLUGIN_DIR + "/scripts" SCHEDULE_JSON = PLUGIN_DIR + "/schedule.json" CUSTOM_CRON = PLUGIN_DIR + "/customSchedule.cron" US_NAME = "WebinarWatcher" CRON_EXPR = "0 8 * * *" # soubory, které kopírujeme na server (telegram_notify.py = přibalená kopie, # protože /scripts/Knihovny na serveru není) FILES = ["watcher.py", "telegram_notify.py", "config.json", "requirements.txt", "NOTES.md"] def connect(): pw = os.environ.get("TOWER_PW") if not pw: sys.exit("Chybí TOWER_PW v prostředí.") c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(HOST, username=USER, password=pw, timeout=20, allow_agent=False, look_for_keys=False) return c def run(c, cmd, timeout=180): _in, out, err = c.exec_command(cmd, timeout=timeout) o = out.read().decode("utf-8", "replace") e = err.read().decode("utf-8", "replace") rc = out.channel.recv_exit_status() return rc, o, e def show(c, cmd, timeout=180): rc, o, e = run(c, cmd, timeout) print(f"$ {cmd}") body = (o + (("\n[stderr] " + e) if e.strip() else "")).rstrip() print(body if body else "(prázdné)") print(f" rc={rc}\n") return rc, o, e # ── RECON ──────────────────────────────────────────────────────────────────── def recon(c): show(c, "hostname; uname -r") show(c, "docker ps --format '{{.Names}}' | sort") show(c, "ls -la /mnt/user/Scripts/ | head -50") show(c, f"docker exec {CONTAINER} ls /scripts/ | head -50") show(c, f"docker exec {CONTAINER} ls -la /scripts/Knihovny/telegram_notify.py") show(c, f"docker exec {CONTAINER} sh -lc 'test -f /scripts/Medevio/.env && grep -oE \"^(TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID)=\" /scripts/Medevio/.env || echo NENI_ENV'") show(c, f"docker exec {CONTAINER} python3 -c \"import requests,bs4;print('deps_ok requests',requests.__version__,'bs4',bs4.__version__)\"") show(c, f"ls -la {USERSCRIPTS}/ | head -50") # vzor existujícího wrapperu + rozvrhu (StahovaniFaktur) show(c, f"cat {USERSCRIPTS}/StahovaniFaktur/script 2>/dev/null") show(c, f"cat {USERSCRIPTS}/StahovaniFaktur/schedule.json 2>/dev/null") show(c, "grep -n 'Scripts' /etc/cron.d/root 2>/dev/null | head") # ── DEPLOY (kopie souborů) ─────────────────────────────────────────────────── def deploy(c): run(c, f"mkdir -p {HOST_DIR} /mnt/user/Scripts/logs") sftp = c.open_sftp() for f in FILES: lp = os.path.join(LOCAL_DIR, f) if not os.path.exists(lp): print(f" přeskakuji (není lokálně): {f}") continue rp = posixpath.join(HOST_DIR, f) sftp.put(lp, rp) print(f" ↑ {f} → {rp}") # seed state.json jen když na serveru ještě není (ať se nepřemazává běhový stav) rp_state = posixpath.join(HOST_DIR, "state.json") rc, _o, _e = run(c, f"test -f {rp_state}") if rc != 0: lp_state = os.path.join(LOCAL_DIR, "state.json") if os.path.exists(lp_state): sftp.put(lp_state, rp_state) print(f" ↑ state.json (seed) → {rp_state}") else: with sftp.open(rp_state, "w") as fh: fh.write('{"last_id": null}\n') print(" ↑ state.json (prázdný)") else: print(" state.json na serveru už existuje — neměním.") sftp.close() show(c, f"ls -la {HOST_DIR}/") # ── ENV (naplní /scripts/Webinare/.env Telegram klíči z lokálního Medevio/.env) ─ def env(c): src = os.path.join(os.path.dirname(LOCAL_DIR), "Medevio", ".env") if not os.path.exists(src): sys.exit("Lokální Medevio/.env nenalezen.") chteji = ("TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID") radky = [] with open(src, encoding="utf-8") as fh: for line in fh: s = line.strip() if "=" in s and not s.startswith("#"): k = s.split("=", 1)[0].strip() if k in chteji: radky.append(s) keys = [r.split("=", 1)[0] for r in radky] if not all(k in keys for k in chteji): sys.exit(f"V Medevio/.env chybí některý z klíčů: {chteji}") run(c, f"mkdir -p {HOST_DIR}") sftp = c.open_sftp() with sftp.open(posixpath.join(HOST_DIR, ".env"), "w") as fh: fh.write("\n".join(radky) + "\n") sftp.chmod(posixpath.join(HOST_DIR, ".env"), 0o600) sftp.close() print(f" .env zapsán na server ({', '.join(keys)}) — hodnoty se nevypisují.") show(c, f"docker exec {CONTAINER} sh -lc 'grep -oE \"^(TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID)=\" {CONT_DIR}/.env'") # ── CRON RECON (zjistí, jak User Scripts ukládá rozvrh) ────────────────────── def cron(c): show(c, "ls -la /boot/config/plugins/user.scripts/scripts/StahovaniFaktur/") show(c, "ls -la /boot/config/plugins/user.scripts/scripts/MedicusFirebirdRestore/") show(c, "cat /boot/config/plugins/user.scripts/scripts/MedicusFirebirdRestore/schedule.json 2>/dev/null || echo bez_schedule_json") show(c, "ls -la /etc/cron.d/") show(c, "cat /etc/cron.d/root 2>/dev/null") show(c, "crontab -l 2>/dev/null | tail -40") # ── CRONSTORE RECON (kam plugin persistuje rozvrh přes reboot) ─────────────── def cronstore(c): show(c, "ls -la /boot/config/plugins/user.scripts/") show(c, "find /boot/config/plugins/user.scripts/ -maxdepth 1 -type f -exec ls -la {} +") show(c, "grep -rsl 'StahovaniFaktur' /boot/config/ 2>/dev/null | grep -v '/scripts/StahovaniFaktur/'") show(c, "grep -rsn '6,18\\|cron\\|schedule' /boot/config/plugins/user.scripts/ --include='*.json' --include='*.cfg' --include='*.dat' --include='*.php' 2>/dev/null | head -40") # ── CRONFILES (dump přesného formátu schedule.json + customSchedule.cron) ──── def cronfiles(c): show(c, "sed -n '185,210p' /boot/config/plugins/user.scripts/schedule.json") show(c, "head -8 /boot/config/plugins/user.scripts/schedule.json") show(c, "tail -8 /boot/config/plugins/user.scripts/schedule.json") show(c, "cat /boot/config/plugins/user.scripts/customSchedule.cron") show(c, "ls -la /usr/local/sbin/update_cron /usr/local/emhttp/plugins/user.scripts/startCustom.php 2>&1") # ── SMOKE TEST (neblokuje na Telegramu) ────────────────────────────────────── def smoke(c): # ověří přibalený telegram modul + načtení .env (jen délky, ne hodnoty) # + detekci webináře na webu. NEodesílá Telegram ani registraci. py = ( "import sys; sys.path.insert(0,'/scripts/Webinare');" "import telegram_notify as t;" "print('telegram .env OK: token_len',len(t._token()),'chat_id_set',bool(t._resolve_chat_id(None)));" "import json,requests,re;" "from bs4 import BeautifulSoup;" "cfg=json.load(open('/scripts/Webinare/config.json',encoding='utf-8'));" "s=requests.Session(); s.get(cfg['watch_url'],headers={'User-Agent':'Mozilla/5.0'},timeout=30);" "r=s.get(cfg['watch_url'],headers={'User-Agent':'Mozilla/5.0'},timeout=30);" "a=BeautifulSoup(r.text,'html.parser').select('a[href*=\\\"webinar.php?idwebinar=\\\"]')[0];" "print('detekce OK webinar=',re.search(r'idwebinar=(\\\\d+)',a['href']).group(1))" ) show(c, f"docker exec {CONTAINER} python3 -c \"{py}\"", timeout=90) # ── SCHEDULE (User Scripts plugin, denně 8:00) ─────────────────────────────── def schedule(c): d = f"{USERSCRIPTS}/{US_NAME}" script_path = f"{d}/script" cron_line = (f"{CRON_EXPR} /usr/local/emhttp/plugins/user.scripts/startCustom.php " f"{script_path} > /dev/null 2>&1") # wrapper (styl převzat z StahovaniFaktur: flock + docker exec + log s datem/rc) wrapper = ( "#!/bin/bash\n" "# WebinarWatcher - denne 8:00, hlidac webinaru praktickylekar.online. flock proti prekryvu.\n" "LOG=/mnt/user/Scripts/logs/webinar_watcher.log\n" "mkdir -p /mnt/user/Scripts/logs\n" "exec 9>/tmp/webinar_watcher.lock\n" "flock -n 9 || exit 0\n" "OUT=$(docker exec -e PYTHONIOENCODING=utf-8 -e TZ=Europe/Prague " + CONTAINER + " python3 " + CONT_DIR + "/watcher.py 2>&1)\n" "RC=$?\n" "{ echo \"===== $(date '+%F %T') (rc=$RC) =====\"; echo \"$OUT\"; } >> \"$LOG\"\n" ) run(c, f"mkdir -p {d}") sftp = c.open_sftp() with sftp.open(script_path, "w") as fh: fh.write(wrapper) with sftp.open(f"{d}/name", "w") as fh: fh.write(US_NAME) with sftp.open(f"{d}/description", "w") as fh: fh.write("Hlidac webinaru praktickylekar.online, denne 8:00") # ── schedule.json: přidej/aktualizuj záznam (se zálohou) ── run(c, f"cp -a {SCHEDULE_JSON} {SCHEDULE_JSON}.bak_webinar") with sftp.open(SCHEDULE_JSON, "r") as fh: data = json.loads(fh.read().decode("utf-8")) data[script_path] = { "script": script_path, "frequency": "custom", "id": "schedule" + US_NAME, "custom": CRON_EXPR, } with sftp.open(SCHEDULE_JSON, "w") as fh: fh.write(json.dumps(data, indent=2)) # ── customSchedule.cron: přidej řádek (se zálohou), pokud chybí ── with sftp.open(CUSTOM_CRON, "r") as fh: cron_txt = fh.read().decode("utf-8") if script_path not in cron_txt: run(c, f"cp -a {CUSTOM_CRON} {CUSTOM_CRON}.bak_webinar") with sftp.open(CUSTOM_CRON, "w") as fh: fh.write(cron_txt.rstrip() + "\n\n" + cron_line + "\n") sftp.close() run(c, f"chmod +x {script_path}") # ── regeneruj systémový cron + ověř ── show(c, "/usr/local/sbin/update_cron") print("── OVĚŘENÍ ──") show(c, f"ls -la {d}/") show(c, f"grep -n '{US_NAME}' {CUSTOM_CRON}") show(c, f"grep -n '{US_NAME}' /etc/cron.d/root") show(c, f"grep -n '{US_NAME}' {SCHEDULE_JSON}") # ── PRODRUN (spustí přesně to, co pustí cron — pro ruční test/trigger) ──────── def prodrun(c): show(c, f"docker exec -e PYTHONIOENCODING=utf-8 {CONTAINER} python3 {CONT_DIR}/watcher.py", timeout=200) MODES = {"recon": recon, "deploy": deploy, "env": env, "cron": cron, "cronstore": cronstore, "cronfiles": cronfiles, "smoke": smoke, "schedule": schedule, "prodrun": prodrun} def main(): mode = sys.argv[1] if len(sys.argv) > 1 else "recon" if mode not in MODES: sys.exit(f"Neznámý režim '{mode}'. Použij: {', '.join(MODES)}") c = connect() try: print(f"=== {mode.upper()} na {USER}@{HOST} ===\n") MODES[mode](c) finally: c.close() if __name__ == "__main__": main()