hosts_app/app.py
2025-03-10 23:32:43 +01:00

1836 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Flask, render_template, request, redirect, url_for, flash, session, Response, jsonify
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import os, paramiko, threading, time, io, tempfile, csv
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime, timezone, timedelta
from io import StringIO
import socket
import ipaddress
import difflib
from croniter import croniter
from tzlocal import get_localzone
from werkzeug.serving import WSGIRequestHandler
WSGIRequestHandler.server_version = ""
WSGIRequestHandler.sys_version = ""
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SECRET_KEY'] = 'supersecretkey'
db = SQLAlchemy(app)
# MODELE
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
hosts = db.relationship('Host', backref='user', lazy=True)
hostfiles = db.relationship('HostFile', backref='user', lazy=True)
settings = db.relationship('UserSettings', backref='user', uselist=False)
class Host(db.Model):
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(255), nullable=False)
username = db.Column(db.String(100), nullable=False)
password = db.Column(db.String(200), nullable=False)
type = db.Column(db.String(50), default='linux') # 'linux' albo 'mikrotik'
auth_method = db.Column(db.String(20), default='password')
private_key = db.Column(db.Text, nullable=True)
key_passphrase = db.Column(db.String(200), nullable=True)
port = db.Column(db.Integer, default=22)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
auto_deploy_enabled = db.Column(db.Boolean, default=True)
auto_backup_enabled = db.Column(db.Boolean, default=True)
preferred_hostfile_id = db.Column(db.Integer, db.ForeignKey('host_file.id'), nullable=True)
preferred_hostfile = db.relationship('HostFile', foreign_keys=[preferred_hostfile_id])
use_daemon = db.Column(db.Boolean, default=False)
daemon_url = db.Column(db.String(255), nullable=True)
daemon_token = db.Column(db.String(255), nullable=True)
disable_regex_deploy = db.Column(db.Boolean, default=False)
disable_local_default = db.Column(db.Boolean, default=False)
@property
def resolved_hostname(self):
try:
return socket.gethostbyaddr(self.hostname)[0]
except Exception:
return self.hostname
@property
def resolved_daemon(self):
if self.daemon_url:
try:
daemon_str = self.daemon_url.split("://")[-1]
daemon_ip = daemon_str.split(":")[0]
return socket.gethostbyaddr(daemon_ip)[0]
except Exception:
return daemon_ip
return ""
@property
def raw_ip(self):
if self.use_daemon and self.type == 'linux' and self.daemon_url:
daemon_str = self.daemon_url.split("://")[-1]
daemon_ip = daemon_str.split(":")[0]
return daemon_ip
return self.hostname
class DeployLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
details = db.Column(db.Text, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship('User', backref='deploy_logs', lazy=True)
class HostFile(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(100), nullable=False, default='Default Hosts')
content = db.Column(db.Text, nullable=False)
class UserSettings(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False)
auto_deploy_enabled = db.Column(db.Boolean, default=False)
#deploy_interval = db.Column(db.Integer, default=60) # interwał wdrożeń (minuty)
#backup_interval = db.Column(db.Integer, default=60) # interwał backupów (minuty)
deploy_cron = db.Column(db.String(100), default="12 12 * * *")
backup_cron = db.Column(db.String(100), default="12 12 * * *")
auto_backup_enabled = db.Column(db.Boolean, default=False)
last_deploy_time = db.Column(db.DateTime, nullable=True)
regex_deploy_enabled = db.Column(db.Boolean, default=True)
backup_retention_days = db.Column(db.Integer, default=0)
global_ssh_key = db.Column(db.Text, nullable=True)
global_key_passphrase = db.Column(db.String(200), nullable=True)
class Backup(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
host_id = db.Column(db.Integer, db.ForeignKey('host.id'), nullable=True)
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
content = db.Column(db.Text, nullable=False)
description = db.Column(db.String(255), nullable=True)
host = db.relationship('Host', backref='backups', lazy=True)
class RegexHostEntry(db.Model):
__tablename__ = 'regex_host_entry'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
cidr_range = db.Column(db.String(50), nullable=False) # np. '10.87.200.0/27'
gateway_ip = db.Column(db.String(50), nullable=True) # np. '10.87.200.30'
gateway_hostname = db.Column(db.String(255), nullable=True) # np. 'gw'
domain_suffix = db.Column(db.String(255), nullable=False, default="guest.domain.com")
host_prefix = db.Column(db.String(255), nullable=False, default="user")
use_gateway_ip = db.Column(db.Boolean, default=False)
comment = db.Column(db.String(255), nullable=True)
user = db.relationship('User', backref='regex_entries')
class HostFileVersion(db.Model):
id = db.Column(db.Integer, primary_key=True)
hostfile_id = db.Column(db.Integer, db.ForeignKey('host_file.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
hostfile = db.relationship('HostFile', backref=db.backref('versions', lazy=True))
class LocalDefaultEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
ip_address = db.Column(db.String(50), nullable=False)
hostname = db.Column(db.String(255), nullable=False)
entry = db.Column(db.Text, nullable=False) # To pole już istnieje
user = db.relationship('User', backref='local_defaults')
# Funkcje pomocnicze
def ensure_local_defaults(content, user_id):
default_entries = LocalDefaultEntry.query.filter_by(user_id=user_id).all()
required_lines = []
for entry in default_entries:
if entry.ip_address and entry.hostname:
required_lines.append(f"{entry.ip_address} {entry.hostname}".strip())
lines = [l.rstrip() for l in content.splitlines() if l.strip()]
lines = [line for line in lines if not any(line.startswith(e.split()[0]) for e in required_lines)]
lines = required_lines + lines
final_content = "\n".join(lines) + "\n"
return final_content
def format_host(host):
if not host:
return "Unknown Host"
resolved_name = None
# Priorytet dla Linux Daemon
if host.use_daemon and host.type == 'linux' and host.daemon_url:
resolved_name = host.resolved_daemon or host.hostname
# Dla standardowych hostów używamy resolved_hostname
if not resolved_name:
resolved_name = host.resolved_hostname or host.hostname
# Jeśli resolved_name nadal jest IP, spróbuj rozwiązać przez DNS
if resolved_name == host.raw_ip:
try:
resolved_name = socket.gethostbyaddr(host.raw_ip)[0]
except (socket.herror, socket.gaierror):
pass # Jeśli nie można rozwiązać, pozostaw IP
return f"{resolved_name} ({host.raw_ip})" if resolved_name else f"Unknown ({host.raw_ip})"
def wrap_content_with_comments(content):
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
header_comment = f"# Auto-hosts upload: {now_str}\n"
footer_comment = f"\n# End of auto-hosts upload: {now_str}\n"
return header_comment + content + footer_comment
def open_ssh_connection(host_obj):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if host_obj.auth_method in ['ssh_key', 'global_key']:
if host_obj.auth_method == 'ssh_key':
key_str = host_obj.private_key
key_passphrase = host_obj.key_passphrase if host_obj.key_passphrase else None
else: # global_key
# Pobieramy globalny klucz z ustawień użytkownika
user_settings = UserSettings.query.filter_by(user_id=host_obj.user_id).first()
if not user_settings or not user_settings.global_ssh_key:
raise Exception("Globalny klucz SSH nie został ustawiony w ustawieniach.")
key_str = user_settings.global_ssh_key
key_passphrase = user_settings.global_key_passphrase if user_settings.global_key_passphrase else None
key_file_obj = io.StringIO(key_str)
try:
pkey = paramiko.RSAKey.from_private_key(key_file_obj, password=key_passphrase)
except paramiko.SSHException as e:
raise Exception(f"Error reading private key: {str(e)}")
ssh.connect(
hostname=host_obj.hostname,
port=host_obj.port,
username=host_obj.username,
pkey=pkey,
timeout=10,
banner_timeout=30
)
else:
ssh.connect(
hostname=host_obj.hostname,
port=host_obj.port,
username=host_obj.username,
password=host_obj.password,
timeout=10,
banner_timeout=30
)
return ssh
def get_statistics(user_id):
user = db.session.get(User, user_id)
host_count = Host.query.filter_by(user_id=user_id).count()
logs = DeployLog.query.all()
filtered_logs = [log for log in logs if f"for user {user_id}" in log.details or f"for {user.username}" in log.details]
total_deployments = len(filtered_logs)
successful_deployments = len([log for log in filtered_logs if "Updated" in log.details or "Deployed" in log.details])
failed_deployments = len([log for log in filtered_logs if "Failed" in log.details])
return {
"host_count": host_count,
"total_deployments": total_deployments,
"successful_deployments": successful_deployments,
"failed_deployments": failed_deployments
}
def automated_backup_for_host(host):
try:
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
resp = requests.get(url, headers=headers, timeout=10, verify=False)
if resp.status_code != 200:
raise Exception(f"Daemon GET error: {resp.status_code} - {resp.text}")
data = resp.json()
content = data.get("hosts", "")
# Używamy format_host, aby log był zgodny z deploy
backup_info = f"[BACKUP] Automatic backup created for server {format_host(host)}"
else:
if host.type == 'mikrotik':
ssh = open_ssh_connection(host)
stdin, stdout, stderr = ssh.exec_command("/ip dns static export")
content = stdout.read().decode('utf-8')
ssh.close()
backup_info = f"[BACKUP] Automatic backup created for server {format_host(host)}"
else:
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with sftp.open('/etc/hosts', 'r') as remote_file:
content = remote_file.read().decode('utf-8')
sftp.close()
ssh.close()
backup_info = f"[BACKUP] Automatic backup created for server {format_host(host)}"
backup = Backup(
user_id=host.user_id,
host_id=host.id,
content=content,
description=f'Backup from server {format_host(host)} at {datetime.now(timezone.utc).isoformat()}'
)
db.session.add(backup)
db.session.commit()
log_entry = DeployLog(details=backup_info, user_id=host.user_id)
db.session.add(log_entry)
db.session.commit()
print(f'Automated backup for server {format_host(host)} created successfully.')
except Exception as e:
print(f'Error creating automated backup for server {format_host(host)}: {str(e)}')
def automated_backups():
with app.app_context():
now = datetime.now(timezone.utc)
hosts = Host.query.all()
for host in hosts:
# Dodaj warunek: backup dla danego hosta ma być wykonywany tylko, jeśli jest włączony
if not host.auto_backup_enabled:
continue
settings = UserSettings.query.filter_by(user_id=host.user_id).first()
if not settings or not settings.auto_backup_enabled or not settings.backup_cron:
continue
# Pobieramy ostatni backup dla hosta
last_backup = Backup.query.filter_by(user_id=host.user_id, host_id=host.id)\
.order_by(Backup.created_at.desc()).first()
if last_backup:
base_time = last_backup.created_at
if base_time.tzinfo is None:
base_time = base_time.replace(tzinfo=timezone.utc)
else:
base_time = datetime.now(timezone.utc) - timedelta(minutes=1)
cron = croniter(settings.backup_cron, base_time)
next_backup_time = cron.get_next(datetime)
if now >= next_backup_time:
automated_backup_for_host(host)
db.session.commit()
def wrap_content_with_comments(content):
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
header_comment = f"# Auto-hosts upload: {now_str}\n"
footer_comment = f"\n# End of auto-hosts upload: {now_str}\n"
return header_comment + content + footer_comment
def wrap_mikrotik_content(content):
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H:%M:%S_UTC")
header = f"127.0.0.99 #Auto-hosts_upload:{now_str}"
footer = f"127.0.0.199 #End_of_auto-hosts_upload:{now_str}"
return header + "\n" + content + "\n" + footer
def clear_linux(host, content):
"""Zastępuje /etc/hosts na hoście Linux zawartością `content`."""
ssh = open_ssh_connection(host)
import tempfile, os
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(content)
tmp_file_path = tmpf.name
sftp = ssh.open_sftp()
sftp.put(tmp_file_path, '/etc/hosts')
sftp.close()
ssh.close()
os.remove(tmp_file_path)
def clear_mikrotik(host):
ssh = open_ssh_connection(host)
ssh.exec_command("/ip dns static remove [find]")
ssh.close()
def generate_regex_hosts(user_id):
entries = RegexHostEntry.query.filter_by(user_id=user_id).all()
lines = []
for entry in entries:
try:
network = ipaddress.ip_network(entry.cidr_range, strict=False)
all_hosts = list(network.hosts()) # IP w puli
host_index = 1
for ip_addr in all_hosts:
if (entry.use_gateway_ip
and entry.gateway_ip
and str(ip_addr) == entry.gateway_ip):
if entry.gateway_hostname:
hostname = f"{entry.gateway_hostname}.{entry.domain_suffix}"
else:
hostname = f"gw.{entry.domain_suffix}"
lines.append(f"{ip_addr} {hostname}")
else:
hostname = f"{entry.host_prefix}{host_index}.{entry.domain_suffix}"
lines.append(f"{ip_addr} {hostname}")
host_index += 1
except ValueError:
pass
return "\n".join(lines) + "\n" if lines else ""
def cleanup_old_backups():
with app.app_context():
all_settings = UserSettings.query.all()
for setting in all_settings:
days = setting.backup_retention_days or 0
if days > 0:
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
old_backups = Backup.query.filter(
Backup.user_id == setting.user_id,
Backup.created_at < cutoff_date
).all()
for b in old_backups:
db.session.delete(b)
db.session.commit()
def format_host(host):
if host.use_daemon and host.type == 'linux' and host.daemon_url:
resolved_name = host.resolved_daemon or host.hostname
else:
resolved_name = host.resolved_hostname or host.hostname
return f"{resolved_name} ({host.raw_ip})"
def remove_local_defaults(content, user_id):
local_defaults = LocalDefaultEntry.query.filter_by(user_id=user_id).all()
lines = content.splitlines()
cleaned_lines = []
for line in lines:
stripped_line = line.strip()
if not any(stripped_line.startswith(f"{entry.ip_address} {entry.hostname}") for entry in local_defaults):
cleaned_lines.append(stripped_line)
return "\n".join(cleaned_lines) + "\n"
# -------------------
# LOGOWANIE, REJESTRACJA, ZMIANA HASŁA
# -------------------
@app.route('/')
def index():
if 'user_id' in session:
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
session['user_id'] = user.id
flash('Login successful', 'success')
return redirect(url_for('dashboard'))
flash('Invalid credentials', 'danger')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password_hash = generate_password_hash(request.form['password'])
user = User(username=username, password=password_hash)
db.session.add(user)
db.session.commit()
flash('Registration successful. Please log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/logout')
def logout():
session.pop('user_id', None)
flash('Logged out', 'info')
return redirect(url_for('login'))
@app.route('/change-password', methods=['GET', 'POST'])
def change_password():
if 'user_id' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
user = db.session.get(User, session['user_id'])
new_password = generate_password_hash(request.form['password'])
user.password = new_password
db.session.commit()
flash('Password changed successfully', 'success')
return redirect(url_for('change_password'))
return render_template('change_password.html')
# -------------------
# ZARZĄDZANIE SERWERAMI
# -------------------
@app.route('/add_server', methods=['GET', 'POST'])
def add_server():
if 'user_id' not in session:
return redirect(url_for('login'))
# Pobieramy wszystkie pliki hosts dla użytkownika (do wyboru preferowanego)
user_hostfiles = HostFile.query.filter_by(user_id=session['user_id']).all()
if request.method == 'POST':
hostname = request.form['hostname']
username = request.form['username']
password_val = request.form['password']
host_type = request.form.get('host_type', 'linux')
auth_method = request.form.get('auth_method', 'password')
private_key = request.form.get('private_key', '').strip()
key_passphrase = request.form.get('key_passphrase', '').strip()
port_str = request.form.get('port', '22')
try:
port = int(port_str)
except ValueError:
port = 22
# Używamy danych dla demona, jeśli checkbox zaznaczony
use_daemon = bool(request.form.get('use_daemon'))
daemon_url = request.form.get('daemon_url', '').strip()
daemon_token = request.form.get('daemon_token', '').strip()
# Dla metod 'ssh_key' i 'global_key' dane są rozróżniane:
if auth_method == 'ssh_key':
stored_key = private_key if private_key else None
stored_passphrase = key_passphrase if key_passphrase else None
elif auth_method == 'global_key':
# W przypadku global_key dane lokalne nie są zapisywane będą pobierane z ustawień użytkownika
stored_key = None
stored_passphrase = None
else:
stored_key = None
stored_passphrase = None
# Obsługa preferowanego pliku hosts
preferred_file_id_str = request.form.get('preferred_hostfile_id', '').strip()
if preferred_file_id_str == '':
chosen_file_id = None
else:
try:
chosen_file_id = int(preferred_file_id_str)
except ValueError:
chosen_file_id = None
host = Host(
hostname=hostname,
username=username,
password=password_val,
type=host_type,
auth_method=auth_method,
private_key=stored_key,
key_passphrase=stored_passphrase,
port=port,
user_id=session['user_id'],
use_daemon=use_daemon if host_type == 'linux' else False,
daemon_url=daemon_url if (use_daemon and host_type == 'linux') else None,
daemon_token=daemon_token if (use_daemon and host_type == 'linux') else None,
preferred_hostfile_id=chosen_file_id
)
db.session.add(host)
db.session.commit()
flash('Host added successfully', 'success')
return redirect(url_for('server_list'))
return render_template('add_server.html', user_hostfiles=user_hostfiles)
@app.route('/delete-server/<int:id>')
def delete_server(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if host and host.user_id == session['user_id']:
db.session.delete(host)
db.session.commit()
flash('Host deleted', 'info')
else:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
@app.route('/server-list', methods=['GET'])
def server_list():
if 'user_id' not in session:
return redirect(url_for('login'))
hosts = Host.query.filter_by(user_id=session['user_id']).all()
return render_template('server_list.html', hosts=hosts)
@app.route('/edit-server/<int:id>', methods=['GET', 'POST'])
def edit_server(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
flash('Server not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
# Pobieramy listę plików hosts dla użytkownika (do wyboru preferowanego)
user_hostfiles = HostFile.query.filter_by(user_id=session['user_id']).all()
if request.method == 'POST':
host.hostname = request.form['hostname']
host.username = request.form['username']
new_password = request.form['password'] or ''
if new_password:
host.password = new_password
port_str = request.form.get('port', '22')
try:
host.port = int(port_str)
except ValueError:
host.port = 22
host.type = request.form.get('host_type', 'linux')
host.auth_method = request.form.get('auth_method', 'password')
if host.auth_method == 'ssh_key':
new_private_key = request.form.get('private_key', '').strip()
new_passphrase = request.form.get('key_passphrase', '').strip()
if new_private_key:
host.private_key = new_private_key
if new_passphrase:
host.key_passphrase = new_passphrase
elif host.auth_method == 'global_key':
# Dla global_key wyczyścimy lokalne pola dane będą pobierane z ustawień
host.private_key = None
host.key_passphrase = None
use_daemon = bool(request.form.get('use_daemon'))
daemon_url = request.form.get('daemon_url', '').strip()
daemon_token = request.form.get('daemon_token', '').strip()
if host.type == 'linux' and use_daemon:
host.use_daemon = True
host.daemon_url = daemon_url
host.daemon_token = daemon_token
else:
host.use_daemon = False
host.daemon_url = None
host.daemon_token = None
preferred_file_id_str = request.form.get('preferred_hostfile_id', '').strip()
if preferred_file_id_str == '':
host.preferred_hostfile_id = None
else:
try:
host.preferred_hostfile_id = int(preferred_file_id_str)
except ValueError:
host.preferred_hostfile_id = None
db.session.commit()
flash('Server updated successfully', 'success')
return redirect(url_for('server_list'))
return render_template('edit_server.html', host=host, user_hostfiles=user_hostfiles)
# -------------------
# TESTOWANIE POŁĄCZENIA SSH DLA HOSTA
# -------------------
@app.route('/test-server-connection/<int:id>', methods=['GET'])
def test_server_connection(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
try:
if host.use_daemon and host.type == 'linux':
import requests
headers = {"Authorization": host.daemon_token}
health_url = host.daemon_url.rstrip('/') + '/health'
resp = requests.get(health_url, headers=headers, verify=False, timeout=5)
if resp.status_code == 200:
flash(f'Demon connection successful (health OK) for {format_host(host)}', 'success')
else:
raise Exception(f"Demon health check returned {resp.status_code}")
sysinfo_url = host.daemon_url.rstrip('/') + '/system-info'
sysinfo_resp = requests.get(sysinfo_url, headers=headers, verify=False, timeout=5)
if sysinfo_resp.status_code == 200:
info = sysinfo_resp.json()
msg = (f"System-info for {format_host(host)}: "
f"CPU={info.get('cpu_percent')}%, "
f"MEM={info.get('memory_percent')}%, "
f"DISK={info.get('disk_percent')}%, "
f"UPTIME={info.get('uptime_seconds')}s")
flash(msg, 'info')
else:
raise Exception(f"Demon system-info returned {sysinfo_resp.status_code}")
else:
ssh = open_ssh_connection(host)
ssh.close()
flash(f'SSH connection to {format_host(host)} successful.', 'success')
except Exception as e:
flash(f'Connection failed for {format_host(host)}: {str(e)}', 'danger')
return redirect(url_for('server_list'))
# -------------------
# ROUTE: CZYSZCZENIE HOSTS - CAŁA GRUPA
# -------------------
@app.route('/clear-server', methods=['GET', 'POST'])
def clear_server():
if 'user_id' not in session:
return redirect(url_for('login'))
hosts = Host.query.filter_by(user_id=session['user_id']).all()
return render_template('clear_servers.html', hosts=hosts, format_host=format_host)
@app.route('/clear-single-server/<int:host_id>', methods=['POST'])
def clear_single_server(host_id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, host_id)
if not host or host.user_id != session['user_id']:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('clear_server'))
default_content = ensure_local_defaults("", session['user_id'])
try:
host_name = format_host(host)
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
resp = requests.post(url, json={"hosts": default_content}, headers=headers, verify=False, timeout=10)
if resp.status_code != 200:
raise Exception(f"Daemon update error: {resp.status_code} - {resp.text}")
elif host.type == 'mikrotik':
clear_mikrotik(host)
else:
clear_linux(host, default_content)
flash(f'Cleared host: {host_name}', 'success')
except Exception as e:
flash(f'Error clearing host {host_name}: {str(e)}', 'danger')
return redirect(url_for('clear_server'))
@app.route('/clear-all-server', methods=['GET', 'POST'])
def clear_all_server():
if 'user_id' not in session:
return redirect(url_for('login'))
hosts = Host.query.filter_by(user_id=session['user_id']).all()
if request.method == 'POST':
linux_clear = request.form.get('linux')
mikrotik_clear = request.form.get('mikrotik')
default_content = ensure_local_defaults("", session['user_id'])
for h in hosts:
try:
host_name = format_host(h) if h else "Unknown Host"
print(f"DEBUG: Czyszczenie hosta {host_name}")
if h.type == 'linux' and linux_clear:
if h.use_daemon:
import requests
url = h.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": h.daemon_token}
resp = requests.post(url, json={"hosts": default_content},
headers=headers, verify=False, timeout=10)
if resp.status_code != 200:
raise Exception(f"Daemon update error: {resp.status_code} - {resp.text}")
else:
clear_linux(h, default_content)
flash(f'Cleared Linux host: {host_name}', 'success')
elif h.type == 'mikrotik' and mikrotik_clear:
clear_mikrotik(h)
flash(f'Cleared Mikrotik host: {host_name}', 'success')
except Exception as e:
flash(f'Error clearing host {host_name}: {str(e)}', 'danger')
return redirect(url_for('clear_all_server'))
return render_template('clear_servers.html', hosts=hosts, format_host=format_host)
# -------------------
# ZARZĄDZANIE PLIKAMI HOSTS (WIELOKROTNE PLIKI)
# -------------------
@app.route('/hosts-files')
def list_hosts_files():
if 'user_id' not in session:
return redirect(url_for('login'))
files = HostFile.query.filter_by(user_id=session['user_id']).all()
return render_template('hosts_files.html', files=files)
@app.route('/hosts-files/new', methods=['GET', 'POST'])
def new_hosts_file():
if 'user_id' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
new_file = HostFile(user_id=session['user_id'], title=title, content=content)
db.session.add(new_file)
db.session.commit()
flash('Hosts file created', 'success')
return redirect(url_for('list_hosts_files'))
return render_template('new_edit_hosts_file.html', file=None)
@app.route('/hosts-files/<int:file_id>/edit', methods=['GET', 'POST'])
def edit_hosts_file(file_id):
if 'user_id' not in session:
return redirect(url_for('login'))
file = db.session.get(HostFile, file_id)
if not file or file.user_id != session['user_id']:
flash('File not found or unauthorized', 'danger')
return redirect(url_for('list_hosts_files'))
if request.method == 'POST':
new_title = request.form['title']
new_content = request.form['content']
if file.content != new_content:
version = HostFileVersion(hostfile_id=file.id, content=file.content)
db.session.add(version)
file.title = new_title
file.content = new_content
db.session.commit()
flash('Hosts file updated and previous version saved.', 'success')
return redirect(url_for('list_hosts_files'))
return render_template('new_edit_hosts_file.html', file=file)
@app.route('/hosts-files/<int:file_id>/delete', methods=['GET'])
def delete_hosts_file(file_id):
if 'user_id' not in session:
return redirect(url_for('login'))
file = db.session.get(HostFile, file_id)
if not file or file.user_id != session['user_id']:
flash('File not found or unauthorized', 'danger')
else:
db.session.delete(file)
db.session.commit()
flash('Hosts file deleted', 'info')
return redirect(url_for('list_hosts_files'))
# -------------------
# WDROŻENIE WYBRANEGO PLIKU HOSTS NA WYBRANE SERWERY
# -------------------
@app.route('/deploy-hosts-file/<int:file_id>', methods=['GET', 'POST'])
def deploy_hosts_file(file_id):
if 'user_id' not in session:
return redirect(url_for('login'))
file = db.session.get(HostFile, file_id)
if not file or file.user_id != session['user_id']:
flash('File not found or unauthorized', 'danger')
return redirect(url_for('list_hosts_files'))
hosts = Host.query.filter_by(user_id=session['user_id']).all()
# Dla hostów korzystających z demona obliczamy IP oraz rozwiązaną nazwę
for host in hosts:
if host.use_daemon and host.type == 'linux' and host.daemon_url:
daemon_str = host.daemon_url.split("://")[-1]
daemon_ip = daemon_str.split(":")[0]
host.daemon_ip = daemon_ip
try:
resolved_daemon = socket.gethostbyaddr(daemon_ip)[0]
except Exception:
resolved_daemon = daemon_ip
host._resolved_daemon_local = resolved_daemon
if request.method == 'POST':
selected_host_ids = request.form.getlist('hosts')
for host in hosts:
if str(host.id) in selected_host_ids:
try:
adjusted_content = ensure_local_defaults(file.content)
wrapped_content = wrap_content_with_comments(adjusted_content)
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
resp = requests.post(url, json={"hosts": wrapped_content},
headers=headers, timeout=10, verify=False)
if resp.status_code != 200:
raise Exception(f"Daemon POST error: {resp.status_code} - {resp.text}")
log_details = f'[LINUX/DAEMON] Updated {format_host(host)}'
db.session.add(DeployLog(details=log_details, user_id=session['user_id']))
elif host.type == 'mikrotik':
wrapped_mikro = wrap_mikrotik_content(file.content)
deploy_mikrotik(host, wrapped_mikro)
log_details = f'[MIKROTIK] Updated {format_host(host)}'
db.session.add(DeployLog(details=log_details, user_id=session['user_id']))
else:
ssh = open_ssh_connection(host)
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(wrapped_content)
tmp_file_path = tmpf.name
sftp = ssh.open_sftp()
sftp.put(tmp_file_path, '/etc/hosts')
sftp.close()
ssh.close()
os.remove(tmp_file_path)
log_details = f'[LINUX] Updated {format_host(host)}'
db.session.add(DeployLog(details=log_details, user_id=session['user_id']))
db.session.commit()
flash(f'Deployed file "{file.title}" to {format_host(host)}', 'success')
except Exception as e:
flash(f'Error deploying file "{file.title}" to {format_host(host)}: {str(e)}', 'danger')
return redirect(url_for('list_hosts_files'))
return render_template('deploy_hosts_file.html', file=file, hosts=hosts)
# -------------------
# BACKUP
# -------------------
@app.route('/server-backup/<int:host_id>', methods=['GET'])
def server_backup(host_id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, host_id)
if not host or host.user_id != session['user_id']:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
try:
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
resp = requests.get(url, headers=headers, timeout=10, verify=False)
if resp.status_code != 200:
raise Exception(f"Daemon GET error: {resp.status_code} - {resp.text}")
data = resp.json()
content = data.get("hosts", "")
description = f'Backup from server {format_host(host)} at {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")}'
elif host.type == 'mikrotik':
ssh = open_ssh_connection(host)
stdin, stdout, stderr = ssh.exec_command("/ip dns static export")
content = stdout.read().decode('utf-8')
ssh.close()
description = f'Backup from server {format_host(host)}'
else:
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with sftp.open('/etc/hosts', 'r') as remote_file:
content = remote_file.read().decode('utf-8')
sftp.close()
ssh.close()
description = f'Backup from server {format_host(host)}'
backup = Backup(
user_id=session['user_id'],
host_id=host.id,
content=content,
description=description
)
db.session.add(backup)
db.session.commit()
flash(f'Backup for host {format_host(host)} created successfully.', 'success')
except Exception as e:
flash(f'Error creating backup for host {format_host(host)}: {str(e)}', 'danger')
return redirect(url_for('server_list'))
@app.route('/backups')
def backups():
if 'user_id' not in session:
return redirect(url_for('login'))
backups = Backup.query.filter_by(user_id=session['user_id']).order_by(Backup.created_at.desc()).all()
return render_template('backups.html', backups=backups)
@app.route('/restore-backup/<int:backup_id>', methods=['GET'])
def restore_backup(backup_id):
if 'user_id' not in session:
return redirect(url_for('login'))
backup = db.session.get(Backup, backup_id)
if not backup or backup.user_id != session['user_id']:
flash('Backup not found or unauthorized', 'danger')
return redirect(url_for('backups'))
if backup.host_id:
host = db.session.get(Host, backup.host_id)
if not host or host.user_id != session['user_id']:
flash('Associated host not found or unauthorized', 'danger')
return redirect(url_for('backups'))
try:
if host.type == 'mikrotik':
ssh = open_ssh_connection(host)
ssh.exec_command("/ip dns static remove [find]")
import time
time.sleep(1)
commands = []
for line in backup.content.splitlines():
line = line.strip()
if line.startswith("add "):
commands.append("/ip dns static " + line)
full_command = " ; ".join(commands)
ssh.exec_command(full_command)
ssh.close()
flash(f'Backup restored to {format_host(host)} successfully.', 'success')
else:
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(backup.content)
tmp_file_path = tmpf.name
sftp.put(tmp_file_path, '/etc/hosts')
sftp.close()
ssh.close()
os.remove(tmp_file_path)
flash(f'Backup restored to {format_host(host)} successfully.', 'success')
except Exception as e:
flash(f'Error restoring backup to {format_host(host)}: {str(e)}', 'danger')
else:
hostfile = HostFile.query.filter_by(user_id=session['user_id'], title="Default Hosts").first()
if not hostfile:
hostfile = HostFile(user_id=session['user_id'], title="Default Hosts", content=backup.content)
db.session.add(hostfile)
else:
hostfile.content = backup.content
db.session.commit()
deploy_user(session['user_id'])
flash('Backup restored to default configuration and deployed successfully.', 'success')
return redirect(url_for('backups'))
@app.route('/view-backup/<int:backup_id>', methods=['GET'])
def view_backup(backup_id):
if 'user_id' not in session:
return redirect(url_for('login'))
backup = db.session.get(Backup, backup_id)
if not backup or backup.user_id != session['user_id']:
flash('Backup not found or unauthorized', 'danger')
return redirect(url_for('backups'))
host = None
if backup.host_id:
host = db.session.get(Host, backup.host_id)
return render_template('view_backup.html', backup=backup, host=host)
@app.route('/backup-all', methods=['GET'])
def backup_all():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
hosts = Host.query.filter_by(user_id=user_id).all()
for host in hosts:
try:
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
resp = requests.get(url, headers=headers, timeout=10, verify=False)
if resp.status_code != 200:
raise Exception(f"Daemon GET error: {resp.status_code} - {resp.text}")
data = resp.json()
content = data.get("hosts", "")
description = f'Backup from server {format_host(host)}'
elif host.type == 'mikrotik':
ssh = open_ssh_connection(host)
stdin, stdout, stderr = ssh.exec_command("/ip dns static export")
content = stdout.read().decode('utf-8')
ssh.close()
description = f'Backup from server {format_host(host)}'
else:
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with sftp.open('/etc/hosts', 'r') as remote_file:
content = remote_file.read().decode('utf-8')
sftp.close()
ssh.close()
description = f'Backup from server {format_host(host)}'
backup = Backup(
user_id=user_id,
host_id=host.id,
content=content,
description=description
)
db.session.add(backup)
db.session.commit()
except Exception as e:
flash(f'Error creating backup for {format_host(host)}: {str(e)}', 'danger')
flash('Backup for all hosts created successfully.', 'success')
return redirect(url_for('backups'))
# -------------------
# IMPORT/EXPORT HOSTÓW
# -------------------
@app.route('/export-servers-to-csv', methods=['GET'])
def export_servers_to_csv():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
hosts = Host.query.filter_by(user_id=user_id).all()
si = StringIO()
cw = csv.writer(si)
# Dodajemy wszystkie istotne pola
cw.writerow([
'id', 'hostname', 'username', 'password', 'port', 'type',
'auth_method', 'private_key', 'key_passphrase', 'auto_deploy_enabled',
'auto_backup_enabled', 'preferred_hostfile_id', 'use_daemon', 'daemon_url', 'daemon_token'
])
for host in hosts:
cw.writerow([
host.id,
host.hostname,
host.username,
host.password,
host.port,
host.type,
host.auth_method,
host.private_key or '',
host.key_passphrase or '',
host.auto_deploy_enabled,
host.auto_backup_enabled,
host.preferred_hostfile_id if host.preferred_hostfile_id is not None else '',
host.use_daemon,
host.daemon_url or '',
host.daemon_token or ''
])
output = si.getvalue()
return Response(output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=servers_full.csv"})
@app.route('/import-servers', methods=['GET', 'POST'])
def import_servers():
if 'user_id' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
file = request.files.get('file')
if not file:
flash('No file uploaded', 'danger')
return redirect(url_for('import_servers'))
stream = StringIO(file.stream.read().decode("UTF8"), newline=None)
csv_input = csv.reader(stream)
header = next(csv_input) # zakładamy, że pierwszy wiersz zawiera nagłówki
for row in csv_input:
# Sprawdzamy, czy wiersz zawiera wszystkie wymagane kolumny (w tym przypadku 15)
if len(row) < 15:
continue
# Rozpakowywanie wiersza zgodnie z kolejnością kolumn w eksporcie
(_, hostname, username, password_val, port_str, host_type, auth_method,
private_key, key_passphrase, auto_deploy_enabled, auto_backup_enabled,
preferred_hostfile_id, use_daemon, daemon_url, daemon_token) = row
try:
port = int(port_str)
except ValueError:
port = 22
# Konwersja wartości logicznych
auto_deploy_enabled = auto_deploy_enabled.lower() in ['true', '1', 'yes']
auto_backup_enabled = auto_backup_enabled.lower() in ['true', '1', 'yes']
use_daemon = use_daemon.lower() in ['true', '1', 'yes']
try:
preferred_hostfile_id = int(preferred_hostfile_id) if preferred_hostfile_id else None
except ValueError:
preferred_hostfile_id = None
host = Host(
hostname=hostname,
username=username,
password=password_val,
port=port,
type=host_type,
auth_method=auth_method,
private_key=private_key if private_key else None,
key_passphrase=key_passphrase if key_passphrase else None,
auto_deploy_enabled=auto_deploy_enabled,
auto_backup_enabled=auto_backup_enabled,
preferred_hostfile_id=preferred_hostfile_id,
use_daemon=use_daemon,
daemon_url=daemon_url if daemon_url else None,
daemon_token=daemon_token if daemon_token else None,
user_id=session['user_id']
)
db.session.add(host)
db.session.commit()
flash('Hosts imported successfully', 'success')
return redirect(url_for('server_list'))
return render_template('import_servers.html')
@app.route('/clear-host/<int:id>', methods=['GET'])
def clear_host(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
try:
if host.type == 'linux':
default_content = ensure_local_defaults("")
clear_linux(host, default_content)
elif host.type == 'mikrotik':
clear_mikrotik(host)
flash(f'Cleared host: {host.hostname}', 'success')
except Exception as e:
flash(f'Error clearing host {host.hostname}: {str(e)}', 'danger')
return redirect(url_for('server_list'))
# -------------------
# STRONA USTAWIEŃ (SETTINGS)
# -------------------
@app.route('/settings', methods=['GET', 'POST'])
def settings():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
user_settings = UserSettings.query.filter_by(user_id=user_id).first()
if not user_settings:
user_settings = UserSettings(user_id=user_id)
db.session.add(user_settings)
db.session.commit()
if request.method == 'POST':
auto_deploy = request.form.get('auto_deploy')
deploy_cron = request.form.get('deploy_cron')
auto_backup = request.form.get('auto_backup')
backup_cron = request.form.get('backup_cron')
enable_regex_entries = request.form.get('enable_regex_entries')
retention_val = request.form.get('backup_retention_days', '0')
# Pobierz wartości globalnego klucza SSH z formularza
global_ssh_key = request.form.get('global_ssh_key')
global_key_passphrase = request.form.get('global_key_passphrase')
# Walidacja wyrażeń cron przy pomocy croniter
try:
croniter(deploy_cron)
except Exception as e:
flash("Błędne wyrażenie cron dla deploy: " + str(e), "danger")
return redirect(url_for('settings'))
try:
croniter(backup_cron)
except Exception as e:
flash("Błędne wyrażenie cron dla backup: " + str(e), "danger")
return redirect(url_for('settings'))
user_settings.auto_deploy_enabled = bool(auto_deploy)
user_settings.auto_backup_enabled = bool(auto_backup)
user_settings.deploy_cron = deploy_cron if deploy_cron else "12 12 * * *"
user_settings.backup_cron = backup_cron if backup_cron else "12 12 * * *"
user_settings.regex_deploy_enabled = bool(enable_regex_entries)
try:
user_settings.backup_retention_days = int(retention_val)
except ValueError:
user_settings.backup_retention_days = 0
# Zapis globalnego klucza SSH i passphrase
user_settings.global_ssh_key = global_ssh_key
user_settings.global_key_passphrase = global_key_passphrase
db.session.commit()
flash('Settings updated', 'success')
return redirect(url_for('settings'))
return render_template('settings.html', settings=user_settings)
@app.route('/delete-backup/<int:backup_id>', methods=['POST'])
def delete_backup(backup_id):
if 'user_id' not in session:
return redirect(url_for('login'))
backup = Backup.query.get(backup_id)
if not backup or backup.user_id != session['user_id']:
flash('Backup not found or unauthorized', 'danger')
return redirect(url_for('backups'))
db.session.delete(backup)
db.session.commit()
flash('Backup deleted successfully', 'info')
return redirect(url_for('backups'))
# -------------------
# DEPLOYMENT DOMYŚLNY DLA UŻYTKOWNIKA
# -------------------
def deploy_user(user_id):
user_settings = UserSettings.query.filter_by(user_id=user_id).first()
default_file = HostFile.query.filter_by(user_id=user_id, title="Default Hosts").first()
if not default_file:
return
regex_lines = ""
if user_settings and user_settings.regex_deploy_enabled:
regex_lines = generate_regex_hosts(user_id)
hosts = Host.query.filter_by(user_id=user_id).all()
for h in hosts:
if not h.auto_deploy_enabled:
continue
chosen_file = HostFile.query.filter_by(id=h.preferred_hostfile_id, user_id=user_id).first() if h.preferred_hostfile_id else default_file
final_content = chosen_file.content.strip()
if h.disable_local_default:
final_content = remove_local_defaults(final_content, user_id)
else:
if not h.disable_regex_deploy and regex_lines.strip():
final_content = regex_lines + "\n" + final_content
final_content = ensure_local_defaults(final_content, user_id)
try:
host_name = format_host(h) if h else "Unknown Host"
if h.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(final_content)
deploy_mikrotik(h, wrapped_content)
log_details = f'[MIKROTIK] Updated {host_name} for user {user_id}'
elif h.use_daemon and h.type == 'linux':
import requests
wrapped_content = wrap_content_with_comments(final_content)
url = h.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": h.daemon_token}
resp = requests.post(url, json={"hosts": wrapped_content}, headers=headers, timeout=10, verify=False)
if resp.status_code != 200:
raise Exception(f"Daemon POST error: {resp.status_code} - {resp.text}")
log_details = f'[LINUX/DAEMON] Updated {host_name} for user {user_id}'
else:
ssh = open_ssh_connection(h)
wrapped_content = wrap_content_with_comments(final_content)
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(wrapped_content)
tmp_file_path = tmpf.name
sftp = ssh.open_sftp()
sftp.put(tmp_file_path, '/etc/hosts')
sftp.close()
ssh.close()
os.remove(tmp_file_path)
log_details = f'[LINUX] Updated {host_name} for user {user_id}'
db.session.add(DeployLog(details=log_details, user_id=user_id))
db.session.commit()
except Exception as e:
error_log = f'Failed to update {host_name}: {str(e)} for user {user_id}'
db.session.add(DeployLog(details=error_log, user_id=user_id))
db.session.commit()
def deploy_mikrotik(host, hosts_content):
ssh = open_ssh_connection(host)
stdin, stdout, stderr = ssh.exec_command("/ip dns static export")
exported = stdout.read().decode('utf-8').splitlines()
existing_dns = {}
for line in exported:
line = line.strip()
if not line.startswith('add '):
continue
line = line[4:].strip()
parts = line.split()
address_val = None
name_val = None
for part in parts:
if part.startswith('address='):
address_val = part.replace('address=', '')
elif part.startswith('name='):
name_val = part.replace('name=', '')
if address_val and name_val:
existing_dns[name_val] = address_val
desired_dns = {}
for line in hosts_content.splitlines():
line = line.strip()
if (not line
or line.startswith('#')
or 'Auto-hosts_upload:' in line
or 'End_of_auto-hosts_upload:' in line):
continue
parts = line.split()
if len(parts) < 2:
continue
ip_address = parts[0]
hostnames = parts[1:]
for hname in hostnames:
desired_dns[hname] = ip_address
for name_val, ip_val in desired_dns.items():
if name_val not in existing_dns:
add_cmd = f"/ip dns static add address={ip_val} name={name_val}"
ssh.exec_command(add_cmd)
else:
current_ip = existing_dns[name_val]
if current_ip != ip_val:
remove_cmd = f"/ip dns static remove [find where name={name_val}]"
ssh.exec_command(remove_cmd)
add_cmd = f"/ip dns static add address={ip_val} name={name_val}"
ssh.exec_command(add_cmd)
for existing_name, existing_ip in existing_dns.items():
if existing_name not in desired_dns:
remove_cmd = f"/ip dns static remove [find where name={existing_name}]"
ssh.exec_command(remove_cmd)
ssh.close()
@app.route('/deploy-now')
def deploy_now():
if 'user_id' not in session:
return redirect(url_for('login'))
deploy_user(session['user_id'])
flash('Deployment complete', 'success')
return redirect(url_for('dashboard'))
# -------------------
# DASHBOARD ZE STATYSTYKAMI
# -------------------
@app.route('/dashboard')
def dashboard():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
user = db.session.get(User, user_id)
logs = DeployLog.query.filter_by(user_id=user_id).order_by(DeployLog.timestamp.desc()).all()
stats = get_statistics(user_id)
for log in logs:
log.details = log.details.replace(f" for user {user_id}", "")
#pass
return render_template('dashboard.html', user=user, logs=logs, stats=stats)
# -------------------
# SCHEDULER - AUTOMATYCZNE WDROŻENIA
# -------------------
@app.route('/regex-hosts')
def list_regex_hosts():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
entries = RegexHostEntry.query.filter_by(user_id=user_id).all()
return render_template('list_regex_hosts.html', entries=entries)
@app.route('/regex-hosts/new', methods=['GET', 'POST'])
def new_regex_host():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
if request.method == 'POST':
cidr_range = request.form.get('cidr_range', '').strip()
gateway_ip = request.form.get('gateway_ip', '').strip()
gateway_hostname = request.form.get('gateway_hostname', '').strip()
domain_suffix = request.form.get('domain_suffix', '').strip()
host_prefix = request.form.get('host_prefix', '').strip()
use_gateway_ip = bool(request.form.get('use_gateway_ip'))
comment = request.form.get('comment', '').strip()
if not cidr_range:
flash('Please provide a CIDR range (e.g. 10.87.200.0/27).', 'danger')
return redirect(url_for('new_regex_host'))
if not domain_suffix:
domain_suffix = "domain.com"
if not host_prefix:
host_prefix = "ip"
entry = RegexHostEntry(
user_id=user_id,
cidr_range=cidr_range,
gateway_ip=gateway_ip,
gateway_hostname=gateway_hostname,
domain_suffix=domain_suffix,
host_prefix=host_prefix,
use_gateway_ip=use_gateway_ip,
comment=comment
)
db.session.add(entry)
db.session.commit()
flash('New CIDR entry with optional gateway has been added.', 'success')
return redirect(url_for('list_regex_hosts'))
return render_template('new_edit_regex_host.html', entry=None)
@app.route('/regex-hosts/<int:entry_id>/edit', methods=['GET', 'POST'])
def edit_regex_host(entry_id):
if 'user_id' not in session:
return redirect(url_for('login'))
entry = db.session.get(RegexHostEntry, entry_id)
if not entry or entry.user_id != session['user_id']:
flash('cant find row or row is empty', 'danger')
return redirect(url_for('list_regex_hosts'))
if request.method == 'POST':
cidr_range = request.form.get('cidr_range', '').strip()
gateway_ip = request.form.get('gateway_ip', '').strip()
gateway_hostname = request.form.get('gateway_hostname', '').strip()
domain_suffix = request.form.get('domain_suffix', '').strip()
host_prefix = request.form.get('host_prefix', '').strip()
use_gateway_ip = bool(request.form.get('use_gateway_ip'))
comment = request.form.get('comment', '').strip()
if not cidr_range:
flash('CIDR is required', 'danger')
return redirect(url_for('edit_regex_host', entry_id=entry_id))
entry.cidr_range = cidr_range
entry.gateway_ip = gateway_ip
entry.gateway_hostname = gateway_hostname
entry.domain_suffix = domain_suffix or "domain.com"
entry.host_prefix = host_prefix or "ip"
entry.use_gateway_ip = use_gateway_ip
entry.comment = comment
db.session.commit()
flash('Updated', 'success')
return redirect(url_for('list_regex_hosts'))
return render_template('new_edit_regex_host.html', entry=entry)
@app.route('/regex-hosts/<int:entry_id>/delete', methods=['POST'])
def delete_regex_host(entry_id):
if 'user_id' not in session:
return redirect(url_for('login'))
entry = db.session.get(RegexHostEntry, entry_id)
if not entry or entry.user_id != session['user_id']:
flash('cant find row or row is empty', 'danger')
return redirect(url_for('list_regex_hosts'))
db.session.delete(entry)
db.session.commit()
flash('Row deleted', 'info')
return redirect(url_for('list_regex_hosts'))
@app.route('/delete-selected-backups', methods=['POST'])
def delete_selected_backups():
if 'user_id' not in session:
return redirect(url_for('login'))
selected_ids = request.form.getlist('selected_backups')
for backup_id in selected_ids:
backup = db.session.get(Backup, backup_id)
if backup and backup.user_id == session['user_id']:
db.session.delete(backup)
db.session.commit()
flash('Zaznaczone backupy zostały usunięte.', 'info')
return redirect(url_for('backups'))
@app.route('/update-host-automation/<int:id>', methods=['POST'])
def update_host_automation(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
flash('Serwer nie istnieje lub nie masz uprawnień', 'danger')
return redirect(url_for('server_list'))
setting = request.form.get('setting')
enabled = request.form.get('enabled') == '1'
if setting == 'auto_deploy':
host.auto_deploy_enabled = enabled
elif setting == 'auto_backup':
host.auto_backup_enabled = enabled
elif setting == 'disable_regex':
host.disable_regex_deploy = enabled
elif setting == 'disable_local_default':
host.disable_local_default = enabled
db.session.commit()
flash('Ustawienia automatyzacji zostały zaktualizowane.', 'success')
return redirect(url_for('server_list'))
# -------------------
# EDYCJA LOKALNEGO PLIKU HOSTS
# -------------------
@app.route('/edit-local-hosts', methods=['GET', 'POST'], endpoint='edit_local_hosts')
def edit_local_hosts():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
hostfile = HostFile.query.filter_by(user_id=user_id, title="Default Hosts").first()
if not hostfile:
default_content = "# This is a sample hosts file.\n127.0.0.1 localhost\n"
hostfile = HostFile(user_id=user_id, title="Default Hosts", content=default_content)
db.session.add(hostfile)
db.session.commit()
if request.method == 'POST':
new_content = request.form['hosts_content']
# Zapisz obecną wersję do historii przed zmianą
version = HostFileVersion(hostfile_id=hostfile.id, content=hostfile.content)
db.session.add(version)
# Aktualizacja treści
hostfile.content = new_content
db.session.commit()
flash('Local hosts content updated successfully and previous version saved.', 'success')
return render_template('edit_hosts.html', content=hostfile.content, hostfile=hostfile)
@app.route('/hostfile/<int:hostfile_id>/versions', methods=['GET', 'POST'])
def hostfile_versions(hostfile_id):
if 'user_id' not in session:
return redirect(url_for('login'))
hostfile = HostFile.query.get(hostfile_id)
if not hostfile or hostfile.user_id != session['user_id']:
flash('Hostfile not found or unauthorized', 'danger')
return redirect(url_for('dashboard'))
if request.method == 'POST':
selected_ids = request.form.getlist('selected_versions')
for version_id in selected_ids:
version = HostFileVersion.query.get(version_id)
if version and version.hostfile.user_id == session['user_id']:
db.session.delete(version)
db.session.commit()
flash('Wybrane wersje zostały usunięte.', 'info')
return redirect(url_for('hostfile_versions', hostfile_id=hostfile_id))
versions = HostFileVersion.query.filter_by(hostfile_id=hostfile.id)\
.order_by(HostFileVersion.timestamp.desc()).all()
return render_template('hostfile_versions.html', hostfile=hostfile, versions=versions)
@app.route('/hostfile/<int:hostfile_id>/versions/delete_old/<int:days>')
def delete_old_versions(hostfile_id, days):
if 'user_id' not in session:
return redirect(url_for('login'))
hostfile = HostFile.query.get(hostfile_id)
if not hostfile or hostfile.user_id != session['user_id']:
flash('Hostfile not found or unauthorized', 'danger')
return redirect(url_for('dashboard'))
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
old_versions = HostFileVersion.query.filter(HostFileVersion.hostfile_id == hostfile_id,
HostFileVersion.timestamp < cutoff).all()
for version in old_versions:
db.session.delete(version)
db.session.commit()
flash(f'Usunięto wersje starsze niż {days} dni.', 'info')
return redirect(url_for('hostfile_versions', hostfile_id=hostfile_id))
@app.route('/hostfile/diff_current/<int:hostfile_id>')
def diff_current_hostfile(hostfile_id):
if 'user_id' not in session:
return redirect(url_for('login'))
hostfile = HostFile.query.get(hostfile_id)
if not hostfile or hostfile.user_id != session['user_id']:
flash('Hostfile not found or unauthorized.', 'danger')
return redirect(url_for('dashboard'))
# Używamy len() zamiast |length
if not hostfile.versions or len(hostfile.versions) == 0:
flash('Brak zapisanej historii wersji do porównania.', 'warning')
return redirect(url_for('hostfile_versions', hostfile_id=hostfile_id))
latest_version = hostfile.versions[0]
differ = difflib.HtmlDiff(wrapcolumn=80)
diff_html = differ.make_table(
latest_version.content.splitlines(),
hostfile.content.splitlines(),
fromdesc=f"Najnowsza wersja historii (ID: {latest_version.id}, {latest_version.timestamp.strftime('%Y-%m-%d %H:%M:%S')})",
todesc="Aktualna zawartość",
context=True,
numlines=3
)
return render_template('diff_versions.html', diff_html=diff_html, hostfile_id=hostfile.id)
@app.route('/hostfile/diff/<int:version1_id>/<int:version2_id>')
def diff_hostfile_versions(version1_id, version2_id):
if 'user_id' not in session:
return redirect(url_for('login'))
version1 = HostFileVersion.query.get(version1_id)
version2 = HostFileVersion.query.get(version2_id)
if not version1 or not version2 or version1.hostfile.user_id != session['user_id'] or version2.hostfile.user_id != session['user_id']:
flash('Wersje nie znalezione lub brak uprawnień.', 'danger')
return redirect(url_for('dashboard'))
differ = difflib.HtmlDiff(wrapcolumn=80)
diff_html = differ.make_table(
version1.content.splitlines(),
version2.content.splitlines(),
fromdesc=f"Wersja {version1.id} - {version1.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
todesc=f"Wersja {version2.id} - {version2.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
context=True,
numlines=3
)
hostfile_id = version1.hostfile_id
return render_template('diff_versions.html', diff_html=diff_html, hostfile_id=hostfile_id)
@app.route('/hostfile/version/<int:version_id>')
def view_hostfile_version(version_id):
if 'user_id' not in session:
return redirect(url_for('login'))
version = HostFileVersion.query.get(version_id)
if not version or version.hostfile.user_id != session['user_id']:
flash('Version not found or unauthorized', 'danger')
return redirect(url_for('dashboard'))
return render_template('view_hostfile_version.html', version=version)
@app.route('/hostfile/version/<int:version_id>/restore')
def restore_hostfile_version(version_id):
if 'user_id' not in session:
return redirect(url_for('login'))
version = HostFileVersion.query.get(version_id)
if not version or version.hostfile.user_id != session['user_id']:
flash('Version not found or unauthorized', 'danger')
return redirect(url_for('dashboard'))
hostfile = version.hostfile
hostfile.content = version.content
db.session.commit()
flash('Version restored successfully.', 'success')
return redirect(url_for('edit_local_hosts'))
@app.route('/hostfile/versions')
def default_hostfile_versions():
if 'user_id' not in session:
return redirect(url_for('login'))
# Zakładamy, że domyślny plik hosts ma tytuł "Default Hosts"
hostfile = HostFile.query.filter_by(user_id=session['user_id'], title="Default Hosts").first()
if not hostfile:
flash("Default Hosts file not found.", "danger")
return redirect(url_for('edit_local_hosts'))
return redirect(url_for('hostfile_versions', hostfile_id=hostfile.id))
def scheduled_deployments():
with app.app_context():
now = datetime.now(timezone.utc)
settings_list = UserSettings.query.filter_by(auto_deploy_enabled=True).all()
for setting in settings_list:
if not setting.deploy_cron:
continue
if setting.last_deploy_time:
base_time = setting.last_deploy_time
if base_time.tzinfo is None:
base_time = base_time.replace(tzinfo=timezone.utc)
else:
base_time = datetime(1970,1,1, tzinfo=timezone.utc)
cron = croniter(setting.deploy_cron, base_time)
next_deploy = cron.get_next(datetime)
if now >= next_deploy:
deploy_user(setting.user_id)
setting.last_deploy_time = now
db.session.commit()
@app.route('/server-info/<int:id>', methods=['GET'])
def server_info(id):
if 'user_id' not in session:
return {"error": "Unauthorized"}, 401
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
return {"error": "Host not found or unauthorized"}, 404
if host.use_daemon and host.type == 'linux':
import requests
headers = {"Authorization": host.daemon_token}
sysinfo_url = host.daemon_url.rstrip('/') + '/system-info'
try:
resp = requests.get(sysinfo_url, headers=headers, verify=False, timeout=5)
if resp.status_code == 200:
data = resp.json()
return {
"hostname": host.resolved_daemon if host.daemon_url else host.resolved_hostname,
"ip": host.raw_ip,
"cpu": data.get('cpu_percent'),
"mem": data.get('memory_percent'),
"disk": data.get('disk_percent'),
"uptime_seconds": data.get('uptime_seconds')
}
else:
return {"error": f"Błąd demona: {resp.status_code}"}, 500
except Exception as e:
return {"error": str(e)}, 500
else:
return {"error": "This server do not use daemon."}, 400
@app.errorhandler(404)
def page_not_found(error):
return render_template("404.html", error=error), 404
@app.errorhandler(500)
def internal_server_error(error):
if app.debug:
return render_template("500.html", error=error), 500
@app.route('/local-defaults', methods=['GET', 'POST'])
def local_defaults():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
if request.method == 'POST':
hostname = request.form.get('hostname', '').strip()
ip_address = request.form.get('ip_address', '').strip()
if hostname and ip_address:
entry_content = f"{ip_address} {hostname}"
new_entry = LocalDefaultEntry(user_id=user_id, hostname=hostname, ip_address=ip_address, entry=entry_content)
db.session.add(new_entry)
db.session.commit()
flash('Dodano nowy wpis.', 'success')
else:
flash('Hostname i adres IP są wymagane.', 'danger')
return redirect(url_for('local_defaults'))
entries = LocalDefaultEntry.query.filter_by(user_id=user_id).all()
return render_template('local_defaults.html', entries=entries)
@app.route('/local-defaults/delete/<int:entry_id>', methods=['POST'])
@app.route('/local-defaults/delete', methods=['POST'])
def delete_local_default(entry_id=None):
if 'user_id' not in session:
return jsonify({'status': 'error', 'message': 'Unauthorized'}), 403
if request.is_json:
data = request.get_json()
entry_ids = data.get('entry_ids', [])
if not entry_ids:
return jsonify({'status': 'error', 'message': 'Brak wpisów do usunięcia'}), 400
entries_to_delete = LocalDefaultEntry.query.filter(
LocalDefaultEntry.id.in_(entry_ids),
LocalDefaultEntry.user_id == session['user_id']
).all()
if not entries_to_delete:
return jsonify({'status': 'error', 'message': 'Nie znaleziono wpisów do usunięcia'}), 404
for entry in entries_to_delete:
db.session.delete(entry)
db.session.commit()
return jsonify({'status': 'success', 'message': 'Wpisy usunięte pomyślnie'})
else:
# Obsługa usuwania pojedynczego wpisu
entry = LocalDefaultEntry.query.get(entry_id)
if not entry or entry.user_id != session['user_id']:
flash('Wpis nie istnieje lub brak uprawnień.', 'danger')
return redirect(url_for('local_defaults'))
db.session.delete(entry)
db.session.commit()
flash('Wpis został usunięty.', 'info')
return redirect(url_for('local_defaults'))
@app.route('/local-defaults/update/<int:entry_id>', methods=['POST'])
def update_local_default(entry_id):
if 'user_id' not in session:
return jsonify({'status': 'error', 'message': 'Unauthorized'}), 403
entry = db.session.get(LocalDefaultEntry, entry_id)
if not entry or entry.user_id != session['user_id']:
return jsonify({'status': 'error', 'message': 'Entry not found'}), 404
data = request.get_json()
new_ip = data.get('ip_address', '').strip()
new_hostname = data.get('hostname', '').strip()
if new_ip and new_hostname:
entry.ip_address = new_ip
entry.hostname = new_hostname
entry.entry = f"{new_ip} {new_hostname}"
db.session.commit()
return jsonify({'status': 'success', 'message': 'Updated successfully', 'ip_address': new_ip, 'hostname': new_hostname})
else:
return jsonify({'status': 'error', 'message': 'IP and hostname are required'}), 400
scheduler = BackgroundScheduler(timezone=get_localzone())
scheduler.add_job(func=scheduled_deployments, trigger="interval", minutes=1, next_run_time=datetime.now())
scheduler.add_job(func=automated_backups, trigger="interval", minutes=1, next_run_time=datetime.now())
scheduler.add_job(func=cleanup_old_backups, trigger="interval", hours=24, next_run_time=datetime.now())
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(
host='0.0.0.0',
port=5580,
use_reloader=False,
)