functions and interface
This commit is contained in:
220
app.py
220
app.py
@@ -5,7 +5,6 @@ from flask import Flask, render_template, request, redirect, url_for, flash, ses
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import librouteros
|
||||
import threading
|
||||
@@ -16,7 +15,17 @@ from email.mime.text import MIMEText
|
||||
from flask import current_app as app
|
||||
from flask import render_template
|
||||
import atexit
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import render_template, flash
|
||||
import logging
|
||||
import re
|
||||
try:
|
||||
from dateutil import parser as date_parser
|
||||
except ImportError:
|
||||
date_parser = None
|
||||
|
||||
|
||||
# Konfiguracja aplikacji
|
||||
app = Flask(__name__)
|
||||
@@ -98,6 +107,14 @@ class Anomaly(db.Model):
|
||||
resolved = db.Column(db.Boolean, default=False)
|
||||
device = db.relationship('Device', backref='anomalies')
|
||||
|
||||
class ChangelogEntry(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
version = db.Column(db.String(50), nullable=False)
|
||||
details = db.Column(db.Text, nullable=False)
|
||||
category = db.Column(db.String(10), nullable=False) # "6.x" or "7.x"
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
release_type = db.Column(db.String(10), nullable=False, default="stable")
|
||||
|
||||
# Inicjalizacja bazy (utworzyć bazę przy pierwszym uruchomieniu)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
@@ -272,6 +289,11 @@ def get_email_template(subject, message):
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
def clean_old_changelogs():
|
||||
with app.app_context():
|
||||
cutoff_time = datetime.utcnow() - timedelta(days=7)
|
||||
db.session.query(ChangelogEntry).filter(ChangelogEntry.timestamp < cutoff_time).delete()
|
||||
db.session.commit()
|
||||
|
||||
def check_all_devices():
|
||||
with app.app_context():
|
||||
@@ -318,6 +340,119 @@ def clean_old_logs():
|
||||
Log.query.filter(Log.user_id == setting.user_id, Log.timestamp < cutoff).delete()
|
||||
db.session.commit()
|
||||
|
||||
def parse_release_date(text):
|
||||
"""
|
||||
Próbuje wyłuskać datę z tekstu.
|
||||
Najpierw używa regex, a w razie niepowodzenia – dateutil (jeśli dostępny).
|
||||
"""
|
||||
date_match = re.search(r"\((\d{4})-([A-Za-z]{3})-(\d{2})", text)
|
||||
if date_match:
|
||||
try:
|
||||
return datetime.strptime(f"{date_match.group(1)}-{date_match.group(2)}-{date_match.group(3)}", "%Y-%b-%d")
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd parsowania daty przy użyciu strptime: {e}")
|
||||
if date_parser:
|
||||
try:
|
||||
return date_parser.parse(text, fuzzy=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd parsowania daty przy użyciu dateutil: {e}")
|
||||
return None
|
||||
|
||||
def get_release_type(version_text):
|
||||
"""
|
||||
Określa typ wydania na podstawie numeru wersji:
|
||||
- Jeśli w tekście występuje "beta" – zwraca "beta"
|
||||
- Jeśli w tekście występuje "rc" – zwraca "rc"
|
||||
- W przeciwnym wypadku – "stable"
|
||||
"""
|
||||
lower = version_text.lower()
|
||||
if "beta" in lower:
|
||||
return "beta"
|
||||
elif "rc" in lower:
|
||||
return "rc"
|
||||
else:
|
||||
return "stable"
|
||||
|
||||
def fetch_changelogs(force=False):
|
||||
changelog_url = "https://mikrotik.com/download/changelogs"
|
||||
current_date = datetime.utcnow()
|
||||
|
||||
try:
|
||||
logging.info(f"Pobieranie changelogów z {changelog_url}...")
|
||||
response = requests.get(changelog_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
changelog_sections = soup.find_all("li", class_="accordion-navigation")
|
||||
logging.info(f"Znaleziono {len(changelog_sections)} sekcji changelogów.")
|
||||
|
||||
new_entries = 0
|
||||
for section in changelog_sections:
|
||||
a_tag = section.find("a")
|
||||
if not a_tag:
|
||||
continue
|
||||
|
||||
raw_text = a_tag.get_text(strip=True)
|
||||
match = re.match(r"([0-9.]+[a-zA-Z0-9]*)", raw_text)
|
||||
version_text = match.group(1) if match else raw_text
|
||||
|
||||
# Pomijamy wersje, które nie zaczynają się od "6." lub "7."
|
||||
if not (version_text.startswith("6.") or version_text.startswith("7.")):
|
||||
logging.info(f"Pomijam wersję {version_text} – nie jest 6.x ani 7.x")
|
||||
continue
|
||||
|
||||
details_div = section.find("div", class_="content")
|
||||
changelog_file_url = details_div.get("data-url") if details_div else None
|
||||
|
||||
if not changelog_file_url:
|
||||
logging.warning(f"Brak URL changeloga dla wersji {version_text}")
|
||||
continue
|
||||
|
||||
try:
|
||||
changelog_response = requests.get(changelog_file_url, timeout=10)
|
||||
changelog_response.raise_for_status()
|
||||
changelog_lines = changelog_response.text.splitlines()
|
||||
|
||||
if not changelog_lines:
|
||||
logging.warning(f"Pusty changelog dla wersji {version_text}")
|
||||
continue
|
||||
|
||||
first_line = changelog_lines[0].strip()
|
||||
release_date_dt = parse_release_date(first_line)
|
||||
if release_date_dt is None:
|
||||
logging.warning(f"Nie udało się wyłuskać daty dla wersji {version_text}, pomijam ten changelog")
|
||||
continue # Pomijamy wpis bez daty
|
||||
|
||||
changelog_text = "\n".join(changelog_lines).strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd pobierania changeloga z {changelog_file_url}: {e}")
|
||||
continue
|
||||
|
||||
# Filtrowanie: dla 7.x pomijamy wersje starsze niż 1 rok, dla 6.x starsze niż 2 lata
|
||||
if version_text.startswith("7.") and release_date_dt < (current_date - timedelta(days=365)):
|
||||
logging.info(f"Pomijam wersję {version_text} - starsza niż 1 rok")
|
||||
continue
|
||||
if version_text.startswith("6.") and release_date_dt < (current_date - timedelta(days=730)):
|
||||
logging.info(f"Pomijam wersję {version_text} - starsza niż 2 lata")
|
||||
continue
|
||||
|
||||
# Określenie typu wydania (stable, rc, beta)
|
||||
release_type = get_release_type(version_text)
|
||||
|
||||
new_entry = ChangelogEntry(
|
||||
version=version_text,
|
||||
details=changelog_text,
|
||||
category="6.x" if version_text.startswith("6.") else "7.x",
|
||||
timestamp=release_date_dt,
|
||||
release_type=release_type
|
||||
)
|
||||
db.session.add(new_entry)
|
||||
new_entries += 1
|
||||
|
||||
db.session.commit()
|
||||
logging.info(f"Nowe wpisy dodane: {new_entries}")
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd podczas pobierania changelogów: {e}")
|
||||
|
||||
def detect_anomalies():
|
||||
with app.app_context():
|
||||
# Ustal okres analizy, np. ostatnie 24 godziny
|
||||
@@ -363,12 +498,26 @@ with app.app_context():
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=detect_anomalies,
|
||||
func=detect_anomalies,
|
||||
trigger="interval",
|
||||
minutes=60,
|
||||
id="detect_anomalies",
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=clean_old_changelogs,
|
||||
trigger="interval",
|
||||
days=1,
|
||||
id="clean_changelogs",
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=lambda: fetch_changelogs(force=False),
|
||||
trigger="interval",
|
||||
days=1,
|
||||
id="daily_changelog_fetch",
|
||||
max_instances=1
|
||||
)
|
||||
|
||||
app.logger.debug(f"Scheduler initialized with interval: {interval} seconds")
|
||||
|
||||
@@ -376,6 +525,13 @@ scheduler.start()
|
||||
|
||||
# ROUTY APLIKACJI
|
||||
|
||||
@app.template_filter('format_version')
|
||||
def format_version(value):
|
||||
import re
|
||||
# Zamieniamy ciąg, który składa się z: liczba.część_1 + czterocyfrowy rok, na samą część_1.
|
||||
# Przykładowo: "7.182025" => "7.18"
|
||||
return re.sub(r"(\d+\.\d+)\d{4}", r"\1", value)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if current_user.is_authenticated:
|
||||
@@ -389,11 +545,24 @@ def dashboard():
|
||||
pending_updates_count = Device.query.filter_by(update_required=True).count()
|
||||
logs_count = Log.query.count()
|
||||
users_count = User.query.count()
|
||||
anomalies_count = Anomaly.query.filter_by(resolved=False).count()
|
||||
update_history_count = UpdateHistory.query.count()
|
||||
recent_logs = Log.query.order_by(Log.timestamp.desc()).limit(5).all()
|
||||
|
||||
# Pobieramy najnowsze wersje stabilne dla 7.x i 6.x
|
||||
latest_version_7 = ChangelogEntry.query.filter_by(category="7.x", release_type="stable").order_by(ChangelogEntry.timestamp.desc()).first()
|
||||
latest_version_6 = ChangelogEntry.query.filter_by(category="6.x", release_type="stable").order_by(ChangelogEntry.timestamp.desc()).first()
|
||||
|
||||
return render_template('dashboard.html',
|
||||
devices_count=devices_count,
|
||||
pending_updates_count=pending_updates_count,
|
||||
logs_count=logs_count,
|
||||
users_count=users_count)
|
||||
users_count=users_count,
|
||||
anomalies_count=anomalies_count,
|
||||
update_history_count=update_history_count,
|
||||
recent_logs=recent_logs,
|
||||
latest_version_7=latest_version_7,
|
||||
latest_version_6=latest_version_6)
|
||||
|
||||
# Rejestracja
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
@@ -679,9 +848,9 @@ def test_email():
|
||||
flash("Testowy e-mail wysłany.")
|
||||
return redirect(url_for('settings'))
|
||||
|
||||
@app.route('/reset_password', methods=['GET', 'POST'])
|
||||
@app.route('/change_password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def reset_password():
|
||||
def change_password():
|
||||
if request.method == 'POST':
|
||||
old_password = request.form.get('old_password')
|
||||
new_password = request.form.get('new_password')
|
||||
@@ -696,7 +865,7 @@ def reset_password():
|
||||
db.session.commit()
|
||||
flash("Hasło zostało zresetowane.")
|
||||
return redirect(url_for('reset_password'))
|
||||
return render_template('reset_password.html')
|
||||
return render_template('change_password.html')
|
||||
|
||||
@app.route('/logs/clean', methods=['POST'])
|
||||
@login_required
|
||||
@@ -752,6 +921,43 @@ def update_selected_devices():
|
||||
flash("Wybrane urządzenia zostały zaktualizowane.")
|
||||
return redirect(url_for('devices'))
|
||||
|
||||
@app.route('/routeros_changelog')
|
||||
@login_required
|
||||
def routeros_changelog():
|
||||
channel = request.args.get('channel', 'stable') # "stable", "rc" lub "beta"
|
||||
series = request.args.get('series', '7.x') # "7.x" lub "6.x"
|
||||
selected_version = request.args.get('version')
|
||||
|
||||
# Pobieramy wszystkie wpisy dla danego kanału i serii, posortowane malejąco wg daty
|
||||
entries = ChangelogEntry.query.filter_by(release_type=channel, category=series).order_by(ChangelogEntry.timestamp.desc()).all()
|
||||
|
||||
if selected_version:
|
||||
selected_entry = ChangelogEntry.query.filter_by(version=selected_version, release_type=channel, category=series).first()
|
||||
else:
|
||||
selected_entry = entries[0] if entries else None
|
||||
|
||||
return render_template(
|
||||
'routeros_changelog_tabs.html',
|
||||
channel=channel,
|
||||
series=series,
|
||||
entries=entries,
|
||||
selected_entry=selected_entry
|
||||
)
|
||||
|
||||
@app.route('/force_fetch_changelogs')
|
||||
@login_required
|
||||
def force_fetch_changelogs():
|
||||
with app.app_context():
|
||||
# Usuwamy wszystkie stare wpisy
|
||||
db.session.query(ChangelogEntry).delete()
|
||||
db.session.commit()
|
||||
|
||||
# Pobieramy changelogi od nowa
|
||||
fetch_changelogs(force=True)
|
||||
|
||||
flash("Changelog został całkowicie odświeżony.", "success")
|
||||
return redirect(url_for('routeros_changelog'))
|
||||
|
||||
# Zamknięcie harmonogramu przy zatrzymaniu aplikacji
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
|
Reference in New Issue
Block a user