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 @@
+
+ + Usuwanie danych starszych niż ustawione w progu + +
+
+ + +
@@ -80,18 +89,15 @@
+ + {% block scripts %}{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 866efb2..87f7fb6 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -91,7 +91,10 @@
-
Log operacji
+
+ Log operacji + Więcej logów +
diff --git a/templates/logs.html b/templates/logs.html new file mode 100644 index 0000000..1c39fa2 --- /dev/null +++ b/templates/logs.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block head %} + {{ super() }} + + +{% endblock %} + +{% block content %} +
+

Historia logów operacji

+ + +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ + +
+
+
+ + + + + + + + {% for log in logs %} + + + + + {% endfor %} + +
DataWiadomość
{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}
+
+
+ + +
+{% endblock %} + +{% block scripts %} + {{ super() }} + + + + + + +{% endblock %} diff --git a/templates/router_details_v2.html b/templates/router_details_v2.html index a0211fa..b5fa168 100644 --- a/templates/router_details_v2.html +++ b/templates/router_details_v2.html @@ -22,12 +22,12 @@
- +
- +
- Edytuj ustawienia + Edytuj ustawienia
@@ -50,12 +50,12 @@
-
- + @@ -87,19 +87,19 @@ {% endif %}
- + - +
-
@@ -107,7 +107,7 @@
-
@@ -130,12 +130,12 @@
-
- + @@ -155,24 +155,27 @@ - +
{{ b.file_path|basename }} + + {{ b.file_path|basename }} + {{ b.file_path|filesize }} {{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }} - +
-
-
@@ -180,7 +183,7 @@
-
@@ -202,15 +205,11 @@ {% endblock %}