diff --git a/app.py b/app.py index c5c942d..54cfacd 100644 --- a/app.py +++ b/app.py @@ -9,18 +9,16 @@ import re import smtplib import shutil import socket +import hashlib from datetime import datetime -from ftplib import FTP from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email import encoders from flask import jsonify from flask import Flask -import shutil from datetime import datetime from difflib import HtmlDiff -import difflib from datetime import datetime, timedelta from sqlalchemy import text @@ -87,6 +85,8 @@ class Backup(db.Model): file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary' created_at = db.Column(db.DateTime, default=datetime.utcnow) + checksum = db.Column(db.String(64), nullable=True) + class OperationLog(db.Model): __tablename__ = 'operation_logs' __table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli @@ -112,6 +112,7 @@ class GlobalSettings(db.Model): smtp_login = db.Column(db.String(255), nullable=True) smtp_password = db.Column(db.String(255), nullable=True) smtp_notifications_enabled = db.Column(db.Boolean, default=False) + log_retention_days = db.Column(db.Integer, default=7) ############################################################################### # Inicjalizacja bazy @@ -166,6 +167,13 @@ def load_pkey(ssh_key_str: str): except Exception as e: raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e +def compute_checksum(file_path): + sha256 = hashlib.sha256() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256.update(chunk) + return sha256.hexdigest() + ############################################################################### # Funkcje SSH ############################################################################### @@ -229,12 +237,17 @@ def ssh_backup(router: Router, backup_name: str) -> str: print(f"[DEBUG] ssh_backup -> local_path={local_path}") return local_path -def ssh_upload_backup(router: Router, local_backup_path: str): - print(f"[DEBUG] ssh_upload_backup -> router id={router.id}, local_backup_path={local_backup_path}") +def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum: str = None): + # Weryfikacja sumy kontrolnej, jeśli podana + if expected_checksum: + local_checksum = compute_checksum(local_backup_path) + if local_checksum != expected_checksum: + raise ValueError("Suma kontrolna pliku nie zgadza się – plik może być uszkodzony.") + client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - # Używamy indywidualnego klucza, a jeśli nie ma, to globalnego + # Wybór klucza: indywidualny lub globalny key_source = router.ssh_key if router.ssh_key and router.ssh_key.strip() else get_settings().global_ssh_key if key_source and key_source.strip(): try: @@ -245,8 +258,10 @@ def ssh_upload_backup(router: Router, local_backup_path: str): raise e else: client.connect(router.host, port=router.port, username=router.ssh_user, - password=router.ssh_password, timeout=10, allow_agent=False, look_for_keys=False, banner_timeout=10) + password=router.ssh_password, timeout=10, + allow_agent=False, look_for_keys=False, banner_timeout=10) + # Otwieramy sesję SFTP, przesyłamy plik, a następnie zamykamy połączenie sftp = client.open_sftp() remote_file = os.path.basename(local_backup_path) sftp.put(local_backup_path, remote_file) @@ -254,6 +269,7 @@ def ssh_upload_backup(router: Router, local_backup_path: str): client.close() print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera") + def ssh_test_connection(router: Router) -> dict: """Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname.""" client = paramiko.SSHClient() @@ -504,7 +520,8 @@ def scheduled_auto_binary_backup(): try: backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}" local_path = ssh_backup(r, backup_name) - b = Backup(router_id=r.id, file_path=local_path, backup_type='binary') + checksum = compute_checksum(local_path) + b = Backup(router_id=r.id, file_path=local_path, backup_type='binary', checksum=checksum) db.session.add(b) db.session.commit() notify(s, f"Auto-binary backup dla routera {r.name} OK", True) @@ -720,12 +737,12 @@ def advanced_schedule(): s = get_settings() if request.method == 'POST': s.retention_cron = request.form.get('retention_cron', '').strip() - s.binary_cron = request.form.get('binary_cron', '').strip() # nowe pole + s.binary_cron = request.form.get('binary_cron', '').strip() s.export_cron = request.form.get('export_cron', '').strip() - # Checkbox: jeśli nie jest zaznaczony, nie pojawi się w formularzu, więc ustawiamy na False + s.backup_retention_days = int(request.form.get('backup_retention_days', s.backup_retention_days)) s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False db.session.commit() - reschedule_jobs() # Aktualizuje harmonogram zadań + reschedule_jobs() # Aktualizacja harmonogramu zadań flash("Zaawansowane ustawienia harmonogramu zostały zapisane.") return redirect(url_for('advanced_schedule')) return render_template('advanced_schedule.html', settings=s) @@ -866,7 +883,8 @@ def router_backup(router_id): try: backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}" local_path = ssh_backup(router, backup_name) - b = Backup(router_id=router.id, file_path=local_path, backup_type='binary') + checksum = compute_checksum(local_path) + b = Backup(router_id=router.id, file_path=local_path, backup_type='binary', checksum=checksum) db.session.add(b) db.session.commit() notify(get_settings(), f"Backup {router.name} OK", True) @@ -890,13 +908,19 @@ def upload_backup(router_id, backup_id): if not b: flash("Nie znaleziono backupu binarnego.") return redirect(url_for('router_details', router_id=router.id)) + + # Sprawdź sumę kontrolną pliku przed wgraniem + local_checksum = compute_checksum(b.file_path) + if b.checksum != local_checksum: + flash("Błąd: suma kontrolna backupu nie zgadza się – plik może być uszkodzony.") + return redirect(url_for('router_details', router_id=router.id)) + try: - ssh_upload_backup(router, b.file_path) + ssh_upload_backup(router, b.file_path, expected_checksum=b.checksum) log_operation(f"Backup {os.path.basename(b.file_path)} wgrany do routera {router.name} at {datetime.utcnow()}") flash("Plik backupu wgrany do routera.") except Exception as e: flash(f"Błąd wgrywania: {e}") - #return redirect(url_for('router_details', router_id=router.id)) next_url = request.form.get('next') or request.referrer or url_for('dashboard') return redirect(next_url) @@ -1325,6 +1349,35 @@ def test_connection(router_id): return render_template("test_connection_modal.html", router=router, result=result) return render_template("test_connection.html", router=router, result=result) +@app.route('/logs') +@login_required +def logs_page(): + logs = OperationLog.query.order_by(OperationLog.timestamp.desc()).all() + return render_template('logs.html', logs=logs) + +@app.route('/logs/delete', methods=['POST']) +@login_required +def delete_old_logs(): + try: + delete_days = int(request.form.get('delete_days', 0)) + except ValueError: + flash("Podana wartość jest nieprawidłowa.") + return redirect(url_for('logs_page')) + + if delete_days < 1: + flash("Podaj wartość większą lub równą 1.") + return redirect(url_for('logs_page')) + + cutoff_date = datetime.utcnow() - timedelta(days=delete_days) + old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all() + deleted_count = 0 + for log in old_logs: + db.session.delete(log) + deleted_count += 1 + db.session.commit() + flash(f"Usunięto {deleted_count} logów starszych niż {delete_days} dni.") + return redirect(url_for('logs_page')) + if __name__ == '__main__': with app.app_context(): scheduler = BackgroundScheduler() diff --git a/templates/advanced_schedule.html b/templates/advanced_schedule.html index 7b6ea2d..0de9b1c 100644 --- a/templates/advanced_schedule.html +++ b/templates/advanced_schedule.html @@ -8,6 +8,15 @@