naprawa błędów i nowe funkcje

This commit is contained in:
Mateusz Gruszczyński 2025-02-23 10:31:15 +01:00
parent 31c898ba0c
commit f39a4a9414
6 changed files with 188 additions and 42 deletions

81
app.py
View File

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

View File

@ -8,6 +8,15 @@
<div class="card-body">
<form action="{{ url_for('advanced_schedule') }}" method="POST">
<div class="mb-3">
<div class="mb-3">
<label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
<small>Usuwanie danych starszych niż ustawione w progu</small>
<input type="number" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days }}">
</div>
<div class="mb-3">
<label for="log_retention_days" class="form-label">Próg retencji logów (dni)</label>
<input type="number" class="form-control" id="log_retention_days" name="log_retention_days" value="{{ settings.log_retention_days }}">
</div>
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
@ -80,18 +89,15 @@
</div>
<script>
// Zmienna przechowująca ID pola, do którego ma być wpisane wyrażenie cron
var targetCronField = '';
function openCronModal(fieldId) {
targetCronField = fieldId;
// Wyzeruj wartości w modalu
document.getElementById('cron_minute').value = '*';
document.getElementById('cron_hour').value = '*';
document.getElementById('cron_day').value = '*';
document.getElementById('cron_month').value = '*';
document.getElementById('cron_dow').value = '*';
// Otwórz modal (przy użyciu Bootstrap 5)
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
cronModal.show();
}
@ -102,10 +108,8 @@
var day = document.getElementById('cron_day').value || '*';
var month = document.getElementById('cron_month').value || '*';
var dow = document.getElementById('cron_dow').value || '*';
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
document.getElementById(targetCronField).value = cronExpr;
// Zamknij modal
var modalEl = document.getElementById('cronModal');
var modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();

View File

@ -21,6 +21,9 @@
.diff-add { color: green; }
.diff-rem { color: red; }
</style>
<!-- Blok head umożliwiający dołączenie dodatkowych stylów -->
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
@ -32,8 +35,11 @@
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</a>
<a href="{{ url_for('diff_selector') }}" class="btn btn-secondary me-2">Diff selector</a>
<a href="{{ url_for('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a>
<a href="{{ url_for('logs_page') }}" class="btn btn-secondary me-2">Logi</a>
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</a>
<a href="{{ url_for('advanced_schedule') }}" class="btn btn-secondary me-2">Harmonogram</a>
<a href="{{ url_for('change_password') }}" class="btn btn-secondary me-2">Zmiana hasła</a>
<a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a>
{% else %}
@ -112,5 +118,7 @@
});
}
</script>
<!-- Blok scripts umożliwiający dołączenie dodatkowych skryptów -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -91,7 +91,10 @@
<!-- Log operacji -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title">Log operacji</h5>
<h5 class="card-title">
Log operacji
<a href="{{ url_for('logs_page') }}" class="btn btn-sm btn-outline-primary ms-2">Więcej logów</a>
</h5>
<table class="table table-sm table-bordered">
<thead>
<tr>

73
templates/logs.html Normal file
View File

@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css">
{% endblock %}
{% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Historia logów operacji</h2>
<!-- Formularz usuwania logów starszych od podanej liczby dni -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<form action="{{ url_for('delete_old_logs') }}" method="POST" class="row g-2 align-items-center">
<div class="col-auto">
<label for="delete_days" class="col-form-label">Usuń logi starsze niż:</label>
</div>
<div class="col-auto">
<input type="number" class="form-control" id="delete_days" name="delete_days" min="1" placeholder="Liczba dni" required>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-danger">Usuń logi</button>
</div>
</form>
</div>
</div>
<!-- Tabela logów -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<table id="logsTable" class="table table-striped table-bordered">
<thead class="table-dark">
<tr>
<th>Data</th>
<th>Wiadomość</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ log.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="text-center mt-3">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">Powrót do Dashboard</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<!-- jQuery (jeśli nie jest już dołączone w base.html) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
$('#logsTable').DataTable({
responsive: true,
order: [[0, 'desc']]
});
});
</script>
{% endblock %}

View File

@ -22,12 +22,12 @@
<div class="d-flex flex-wrap gap-2">
<!-- Mniejsze przyciski górne -->
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-primary">Wykonaj /export</button>
<button type="submit" class="btn btn-primary btn-sm">Wykonaj /export</button>
</form>
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-secondary">Wykonaj backup binarny</button>
<button type="submit" class="btn btn-secondary btn-sm">Wykonaj backup binarny</button>
</form>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning">Edytuj ustawienia</a>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning btn-sm">Edytuj ustawienia</a>
</div>
</div>
</div>
@ -50,12 +50,12 @@
<!-- Formularz masowych akcji dla eksportów -->
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
<div class="d-flex justify-content-end">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success">
<button type="submit" name="action" value="download" class="btn btn-success btn-sm">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button>
</div>
</form>
<!-- Tabela z eksportami z podzielonymi kolumnami akcji -->
<!-- Tabela z eksportami -->
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
@ -87,19 +87,19 @@
{% endif %}
</td>
<td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
<i class="bi bi-download"></i>
</a>
</td>
<td>
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-lg btn-outline-primary" title="Podgląd">
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-outline-primary btn-sm" title="Podgląd">
<i class="bi bi-eye"></i>
</a>
</td>
<td>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem">
<button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
<i class="bi bi-envelope"></i>
</button>
</form>
@ -107,7 +107,7 @@
<td>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-danger" title="Usuń">
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
<i class="bi bi-trash"></i>
</button>
</form>
@ -130,12 +130,12 @@
<!-- Formularz masowych akcji dla backupów binarnych -->
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
<div class="d-flex justify-content-end">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success">
<button type="submit" name="action" value="download" class="btn btn-success btn-sm">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button>
</div>
</form>
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji -->
<!-- Tabela z backupami binarnymi -->
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
@ -155,24 +155,27 @@
<td>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
</td>
<td>{{ b.file_path|basename }}</td>
<td>
<!-- Dodaj tooltip z sumą kontrolną -->
<span data-bs-toggle="tooltip" title="Checksum: {{ b.checksum }}">{{ b.file_path|basename }}</span>
</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
<i class="bi bi-download"></i>
</a>
</td>
<td>
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-lg btn-secondary" title="Wgraj do routera">
<button type="submit" class="btn btn-secondary btn-sm" title="Wgraj do routera">
<i class="bi bi-upload"></i>
</button>
</form>
</td>
<td>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem">
<button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
<i class="bi bi-envelope"></i>
</button>
</form>
@ -180,7 +183,7 @@
<td>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-danger" title="Usuń">
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
<i class="bi bi-trash"></i>
</button>
</form>
@ -202,15 +205,11 @@
<script>
document.getElementById('select_all_export').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = e.target.checked;
}
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
document.getElementById('select_all_binary').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = e.target.checked;
}
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
// Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie)
@ -222,5 +221,11 @@ triggerTabList.forEach(function (triggerEl) {
tabTrigger.show();
});
});
// Inicjalizacja tooltipów Bootstrap
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
</script>
{% endblock %}