reporter
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
# DBrestore – Medicus DB restore na Reporteru
|
||||||
|
|
||||||
|
## Co skript dělá
|
||||||
|
|
||||||
|
`restore.py` automaticky obnovuje Firebird databázi Medicus na počítači REPORTER ze zálohy uložené na NAS.
|
||||||
|
|
||||||
|
### Postup krok za krokem
|
||||||
|
|
||||||
|
1. **Kontrola hostitele** – skript lze spustit pouze na počítači REPORTER
|
||||||
|
2. **Najde nejnovější ZIP** v `\\tower\ordinacesynology\MedicusBackup\` (vzor: `MEDICUS_YYMMDD_HHMM.zip`)
|
||||||
|
3. **Kontrola duplicity** – porovná název ZIPu s `last_restored.txt`; pokud už byl obnoven, skončí bez akce
|
||||||
|
4. **Kontrola přenosu (rsync)** – změří velikost ZIPu, počká 60 sekund, změří znovu; pokud se liší, přenos ještě běží → skončí bez akce
|
||||||
|
5. **Rozbalí ZIP** → extrahuje `.fbk` do `C:\Medicus\restore\`
|
||||||
|
6. **Odpojí aktivní klienty** z Firebird DB (přes `MON$ATTACHMENTS`)
|
||||||
|
7. **Smaže starou DB** `C:\Medicus\medicus.fdb` a spustí `gbak` restore s live výpisem
|
||||||
|
8. **Po úspěšném restore:**
|
||||||
|
- uloží název ZIPu do `last_restored.txt`
|
||||||
|
- smaže ZIP ze záložního adresáře
|
||||||
|
- smaže rozbalený `.fbk`
|
||||||
|
- otestuje DB (počet registrovaných pacientů)
|
||||||
|
9. **Odešle email** na `vladimir.buzalka@buzalka.cz` s výsledkem a logem jako přílohou
|
||||||
|
10. **Smaže log**
|
||||||
|
|
||||||
|
## Konfigurace
|
||||||
|
|
||||||
|
| Proměnná | Hodnota |
|
||||||
|
|---|---|
|
||||||
|
| `BACKUP_DIR` | `\\tower\ordinacesynology\MedicusBackup` |
|
||||||
|
| `EXTRACT_DIR` | `C:\Medicus\restore` |
|
||||||
|
| `TARGET_DB` | `C:\Medicus\medicus.fdb` |
|
||||||
|
| `GBAK` | `C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe` |
|
||||||
|
| `LOG_FILE` | `C:\Medicus\restore\restore.log` |
|
||||||
|
| `LAST_RESTORED` | `C:\Medicus\restore\last_restored.txt` |
|
||||||
|
| `EMAIL_TO` | `vladimir.buzalka@buzalka.cz` |
|
||||||
|
|
||||||
|
## Firebird připojení
|
||||||
|
|
||||||
|
```python
|
||||||
|
import fdb
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus\medicus.fdb', # lokálně na REPORTERu
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Z jiných počítačů v síti:
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'reporter:c:\medicus\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scheduled Task
|
||||||
|
|
||||||
|
- **Spouštěcí program:** `C:\Reporting\Python\python.exe`
|
||||||
|
- **Argumenty:** `C:\Reporting\RestoreNaReporter\restore.py`
|
||||||
|
- **Interval:** každou hodinu
|
||||||
|
- **Uživatel:** vlado (s nejvyššími právy)
|
||||||
|
- **DŮLEŽITÉ:** musí být `python.exe`, ne `pythonw.exe` (jinak stdout/stderr nejdou zachytit)
|
||||||
|
|
||||||
|
## Závislosti
|
||||||
|
|
||||||
|
- `fdb` – Firebird Python driver
|
||||||
|
- `msal`, `requests` – pro odesílání emailu přes Microsoft Graph
|
||||||
|
- `EmailMessagingGraph.py` – knihovna v `C:\Reporting\knihovny\`
|
||||||
|
|
||||||
|
Všechny závislosti jsou v `C:\Reporting\Python\`.
|
||||||
|
|
||||||
|
## Produkční umístění
|
||||||
|
|
||||||
|
| Soubor | Cesta |
|
||||||
|
|---|---|
|
||||||
|
| Skript | `C:\Reporting\RestoreNaReporter\restore.py` |
|
||||||
|
| Python | `C:\Reporting\Python\python.exe` |
|
||||||
|
| Email knihovna | `C:\Reporting\knihovny\EmailMessagingGraph.py` |
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
Port **3050 TCP** otevřen pro příchozí spojení (Firebird) – pravidlo "Firebird 3050".
|
||||||
|
|
||||||
|
## Časová osa nočního procesu
|
||||||
|
|
||||||
|
| Čas | Co se děje |
|
||||||
|
|---|---|
|
||||||
|
| 02:00 | Zálohovací skript v ordinaci vytvoří `.zip` |
|
||||||
|
| ~02:30 | Záloha dokončena, rsync začne přenos na NAS |
|
||||||
|
| ~03:xx | rsync dokončen |
|
||||||
|
| každou hodinu | Scheduled task zkontroluje nový ZIP + velikost |
|
||||||
|
| po stabilizaci | Spustí restore (~10 min), odešle email |
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
r"""
|
||||||
|
Rozbalí nejnovější zálohu Medicus z \\tower\ordinacesynology\MedicusBackup\ a obnoví Firebird DB do C:\Medicus\medicus.fdb
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import fdb
|
||||||
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import socket
|
||||||
|
if socket.gethostname().upper() != "REPORTER":
|
||||||
|
raise SystemExit(f"Tento skript lze spustit pouze na pocitaci REPORTER (aktualni: {socket.gethostname()})")
|
||||||
|
|
||||||
|
# --- zachytavani logu ---
|
||||||
|
class Tee:
|
||||||
|
"""Pise na stdout i do bufferu zaroven."""
|
||||||
|
def __init__(self):
|
||||||
|
self.buffer = StringIO()
|
||||||
|
def write(self, msg):
|
||||||
|
sys.__stdout__.write(msg)
|
||||||
|
self.buffer.write(msg)
|
||||||
|
def flush(self):
|
||||||
|
sys.__stdout__.flush()
|
||||||
|
def getvalue(self):
|
||||||
|
return self.buffer.getvalue()
|
||||||
|
|
||||||
|
tee = Tee()
|
||||||
|
sys.stdout = tee
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
BACKUP_DIR = Path(r"\\tower\ordinacesynology\MedicusBackup")
|
||||||
|
EXTRACT_DIR = Path(r"C:\Medicus\restore")
|
||||||
|
TARGET_DB = Path(r"C:\Medicus\medicus.fdb")
|
||||||
|
GBAK = Path(r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe")
|
||||||
|
LOG_FILE = Path(r"C:\Medicus\restore\restore.log")
|
||||||
|
LAST_RESTORED = Path(r"C:\Medicus\restore\last_restored.txt")
|
||||||
|
EMAIL_TO = "vladimir.buzalka@buzalka.cz"
|
||||||
|
USER = "SYSDBA"
|
||||||
|
PASSWORD = "masterkey"
|
||||||
|
|
||||||
|
success = False
|
||||||
|
pocet_pacientu = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1) Najdi nejnovejsi ZIP
|
||||||
|
pattern = re.compile(r"MEDICUS_(\d{6})_(\d{4})\.zip$", re.IGNORECASE)
|
||||||
|
candidates = []
|
||||||
|
for f in BACKUP_DIR.iterdir():
|
||||||
|
m = pattern.match(f.name)
|
||||||
|
if m:
|
||||||
|
ts = datetime.strptime("20" + m.group(1) + m.group(2), "%Y%m%d%H%M")
|
||||||
|
candidates.append((ts, f))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
raise SystemExit("Zadny MEDICUS_*.zip nenalezen v " + str(BACKUP_DIR))
|
||||||
|
|
||||||
|
candidates.sort(reverse=True)
|
||||||
|
latest_ts, latest_zip = candidates[0]
|
||||||
|
print(f"Nejnovejsi zaloha: {latest_zip.name} ({latest_ts})")
|
||||||
|
|
||||||
|
# Zkontroluj jestli uz byl tento ZIP obnoven
|
||||||
|
if LAST_RESTORED.exists():
|
||||||
|
last = LAST_RESTORED.read_text(encoding="utf-8").strip()
|
||||||
|
if last == latest_zip.name:
|
||||||
|
print("Tento backup byl jiz obnoven, novy zip nenalezen. Konec.")
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# Zkontroluj jestli je ZIP kompletne prenesen (rsync)
|
||||||
|
size1 = latest_zip.stat().st_size
|
||||||
|
print(f"Velikost ZIP: {size1 / 1024 / 1024:.1f} MB - cekam 60s...")
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(60)
|
||||||
|
size2 = latest_zip.stat().st_size
|
||||||
|
if size1 != size2:
|
||||||
|
print(f"ZIP se stale meni ({size2 / 1024 / 1024:.1f} MB), prenos nedokoncen. Konec.")
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
raise SystemExit(0)
|
||||||
|
print(f"Velikost stabilni, prenos dokoncen.")
|
||||||
|
|
||||||
|
# 2) Rozbal ZIP - hledej .fbk
|
||||||
|
EXTRACT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(latest_zip) as zf:
|
||||||
|
fbk_entries = [n for n in zf.namelist() if n.lower().endswith(".fbk")]
|
||||||
|
if not fbk_entries:
|
||||||
|
raise SystemExit("ZIP neobsahuje .fbk soubor!")
|
||||||
|
fbk_entry = fbk_entries[0]
|
||||||
|
fbk_name = Path(fbk_entry).name
|
||||||
|
zf.extract(fbk_entry, EXTRACT_DIR)
|
||||||
|
extracted = next(EXTRACT_DIR.rglob(fbk_name))
|
||||||
|
print(f"Rozbaleno: {extracted}")
|
||||||
|
|
||||||
|
# 3) Odpoj aktivni klienty a obnov DB
|
||||||
|
try:
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT MON$ATTACHMENT_ID, MON$USER, MON$REMOTE_PROCESS
|
||||||
|
FROM MON$ATTACHMENTS
|
||||||
|
WHERE MON$ATTACHMENT_ID <> CURRENT_CONNECTION
|
||||||
|
AND MON$USER <> 'GARBAGE COLLECTOR'
|
||||||
|
""")
|
||||||
|
attachments = cur.fetchall()
|
||||||
|
if attachments:
|
||||||
|
print(f"Aktivni pripojeni ({len(attachments)}), odpojuji:")
|
||||||
|
for att_id, user, process in attachments:
|
||||||
|
print(f" - ID={att_id} user={user} process={process}")
|
||||||
|
cur.execute("DELETE FROM MON$ATTACHMENTS WHERE MON$ATTACHMENT_ID = ?", (att_id,))
|
||||||
|
conn.commit()
|
||||||
|
print("Vsechna pripojeni odpojena.\n")
|
||||||
|
else:
|
||||||
|
print("Zadna aktivni pripojeni.\n")
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Varovani: nepodarilo se zkontrolovat pripojeni ({e})\n")
|
||||||
|
|
||||||
|
TARGET_DB.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if TARGET_DB.exists():
|
||||||
|
TARGET_DB.unlink()
|
||||||
|
print(f"Smazana stara DB: {TARGET_DB}")
|
||||||
|
|
||||||
|
cmd = [str(GBAK), "-rep", "-v", str(extracted), str(TARGET_DB), "-user", USER, "-pas", PASSWORD]
|
||||||
|
print(f"\nSpoustim: {' '.join(cmd)}\n")
|
||||||
|
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
for line in proc.stdout:
|
||||||
|
print(line, end="")
|
||||||
|
proc.wait()
|
||||||
|
returncode = proc.returncode
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
minutes, seconds = divmod(int(elapsed), 60)
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
success = True
|
||||||
|
print(f"\nObnova dokoncena. Cas: {minutes}:{seconds:02d} (min:sec)")
|
||||||
|
LAST_RESTORED.write_text(latest_zip.name, encoding="utf-8")
|
||||||
|
latest_zip.unlink()
|
||||||
|
print(f"Smazan ZIP: {latest_zip.name}")
|
||||||
|
extracted.unlink()
|
||||||
|
print(f"Smazan FBK: {extracted.name}")
|
||||||
|
# Test DB - pocet registrovanych pacientu
|
||||||
|
try:
|
||||||
|
conn_test = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur_test = conn_test.cursor()
|
||||||
|
cur_test.execute("""
|
||||||
|
SELECT COUNT(*) FROM KAR
|
||||||
|
WHERE vyrazen = 'N'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT id FROM registr r
|
||||||
|
JOIN icp i ON r.idicp = i.idicp
|
||||||
|
WHERE r.idpac = kar.idpac
|
||||||
|
AND r.datum <= CURRENT_DATE
|
||||||
|
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= CURRENT_DATE)
|
||||||
|
AND r.priznak IN ('V','D','A')
|
||||||
|
AND i.icp = '09305001'
|
||||||
|
AND i.odb = '001'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
pocet_pacientu = cur_test.fetchone()[0]
|
||||||
|
conn_test.close()
|
||||||
|
print(f"Test DB OK - registrovanych pacientu: {pocet_pacientu}")
|
||||||
|
except Exception as e:
|
||||||
|
pocet_pacientu = None
|
||||||
|
print(f"Test DB selhal: {e}")
|
||||||
|
else:
|
||||||
|
print(f"\nCHYBA (navratovy kod {returncode}). Cas: {minutes}:{seconds:02d} (min:sec)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
minutes, seconds = divmod(int(elapsed), 60)
|
||||||
|
print(f"\nVYJIMKA: {e}. Cas: {minutes}:{seconds:02d} (min:sec)")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
|
||||||
|
log_text = tee.getvalue()
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
minutes, seconds = divmod(int(elapsed), 60)
|
||||||
|
|
||||||
|
# Uloz log
|
||||||
|
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
LOG_FILE.write_text(log_text, encoding="utf-8")
|
||||||
|
|
||||||
|
# Odeslat email
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, r"C:\Reporting\knihovny")
|
||||||
|
from EmailMessagingGraph import send_mail
|
||||||
|
|
||||||
|
status = "OK" if success else "CHYBA"
|
||||||
|
subject = f"Medicus DB restore [{status}] - {minutes}:{seconds:02d} min"
|
||||||
|
pac_info = f"\nRegistrovanych pacientu: {pocet_pacientu}" if success and pocet_pacientu is not None else ""
|
||||||
|
body = f"Restore databaze Medicus na Reporteru\n\nVysledek: {status}\nDoba: {minutes}:{seconds:02d} (min:sec){pac_info}\n\nPodrobny log v priloze."
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
to=EMAIL_TO,
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
attachments=LOG_FILE,
|
||||||
|
)
|
||||||
|
print("Email odeslan.")
|
||||||
|
LOG_FILE.unlink(missing_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Chyba pri odesilani emailu: {e}")
|
||||||
Reference in New Issue
Block a user