git add templatesgit add templates OBSLUGA DEMONAgit add templates

This commit is contained in:
Mateusz Gruszczyński
2025-03-06 23:52:44 +01:00
parent cae5ed787d
commit aa0a0a0025
6 changed files with 710 additions and 134 deletions

345
app.py
View File

@@ -44,6 +44,14 @@ class Host(db.Model):
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])
# Funkcja wspolpracy z hosts_daemon
use_daemon = db.Column(db.Boolean, default=False) # <--- NOWE
daemon_url = db.Column(db.String(255), nullable=True) # <--- NOWE
daemon_token = db.Column(db.String(255), nullable=True) # <--- NOWE
@property
def resolved_hostname(self):
try:
@@ -170,12 +178,33 @@ def get_statistics(user_id):
def automated_backup_for_host(host):
try:
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()
if host.use_daemon and host.type == 'linux':
import requests
# pobieramy /etc/hosts z demona:
url = host.daemon_url.rstrip('/') + '/hosts'
# Zmiana: jeśli demon wymaga nagłówka Bearer:
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", "")
else:
# standard:
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()
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 = Backup(
user_id=host.user_id,
host_id=host.id,
@@ -184,13 +213,14 @@ def automated_backup_for_host(host):
)
db.session.add(backup)
db.session.commit()
log_entry = DeployLog(
details=f'[BACKUP] Automatic backup created for host {host.hostname}',
user_id=host.user_id
)
log_entry = DeployLog(details=f'[BACKUP] Automatic backup created for host {host.hostname}',
user_id=host.user_id)
db.session.add(log_entry)
db.session.commit()
print(f'Automated backup for host {host.hostname} created successfully.')
except Exception as e:
print(f'Error creating automated backup for host {host.hostname}: {str(e)}')
@@ -356,6 +386,10 @@ def change_password():
def add_server():
if 'user_id' not in session:
return redirect(url_for('login'))
# Pobieramy wszystkie HostFile tego użytkownika, np. do wyświetlenia w <select>
user_hostfiles = HostFile.query.filter_by(user_id=session['user_id']).all()
if request.method == 'POST':
hostname = request.form['hostname']
username = request.form['username']
@@ -369,8 +403,27 @@ def add_server():
port = int(port_str)
except ValueError:
port = 22
# Czy ma używać demona?
use_daemon = bool(request.form.get('use_daemon'))
daemon_url = request.form.get('daemon_url', '').strip()
daemon_token = request.form.get('daemon_token', '').strip()
# Jeśli auth_method == 'ssh_key'
stored_key = private_key if (auth_method == 'ssh_key' and private_key) else None
stored_passphrase = key_passphrase if (auth_method == 'ssh_key' and key_passphrase) else 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
# Tworzymy nowy obiekt Host
host = Host(
hostname=hostname,
username=username,
@@ -380,14 +433,25 @@ def add_server():
private_key=stored_key,
key_passphrase=stored_passphrase,
port=port,
user_id=session['user_id']
user_id=session['user_id'],
# Obsługa demona tylko jeśli host_type=='linux' i checkbox zaznaczony
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,
# Nowe pole preferowanego pliku
preferred_hostfile_id=chosen_file_id
)
db.session.add(host)
db.session.commit()
flash('Host added successfully', 'success')
# Po dodaniu możesz przekierować do listy serwerów lub pozostawić na formularzu
return redirect(url_for('server_list'))
return render_template('add_server.html')
# GET -> wyświetlamy formularz add_server, przekazując listę user_hostfiles
return render_template('add_server.html', user_hostfiles=user_hostfiles)
@app.route('/delete-server/<int:id>')
def delete_server(id):
@@ -417,27 +481,63 @@ def edit_server(id):
if not host or host.user_id != session['user_id']:
flash('Server not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
# Lista plików usera do <select>:
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']
host.password = request.form['password'] or ''
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')
new_private_key = request.form.get('private_key', '').strip()
new_passphrase = request.form.get('key_passphrase', '').strip()
if host.auth_method == 'ssh_key' and new_private_key:
host.private_key = new_private_key
if host.auth_method == 'ssh_key' and new_passphrase:
host.key_passphrase = new_passphrase
# Demon:
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
# Nowe pole: preferred_hostfile_id
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)
# GET -> renderuj z user_hostfiles
return render_template('edit_server.html', host=host, user_hostfiles=user_hostfiles)
# -------------------
# TESTOWANIE POŁĄCZENIA SSH DLA HOSTA
@@ -446,16 +546,50 @@ def edit_server(id):
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:
ssh = open_ssh_connection(host)
ssh.close()
flash(f'SSH connection to {host.hostname} successful.', 'success')
if host.use_daemon and host.type == 'linux':
# Połączenie przez demon (self-signed certy, verify=False)
import requests
headers = {"Authorization": host.daemon_token}
# Najpierw sprawdzenie /health
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 {host.hostname}', 'success')
else:
raise Exception(f"Demon health check returned {resp.status_code}")
# Dodatkowe pobranie /system-info
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()
# Wyświetlamy kilka przykładowych danych w flash:
msg = (f"System-info for {host.hostname}: "
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:
# Standardowe sprawdzenie przez SSH
ssh = open_ssh_connection(host)
ssh.close()
flash(f'SSH connection to {host.hostname} successful.', 'success')
except Exception as e:
flash(f'SSH connection to {host.hostname} failed: {str(e)}', 'danger')
flash(f'Connection failed for {host.hostname}: {str(e)}', 'danger')
return redirect(url_for('server_list'))
# -------------------
@@ -476,15 +610,29 @@ def clear_single_server(host_id):
if not host or host.user_id != session['user_id']:
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('clear_servers'))
default_content = ensure_local_defaults("")
try:
if host.type == 'linux':
clear_linux(host, default_content)
if host.use_daemon and host.type == 'linux':
import requests
url = host.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": host.daemon_token}
# Zakładamy, że demon potrafi zastąpić /etc/hosts treścią "default_content"
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:
# standard linux
clear_linux(host, default_content)
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('clear_all_server'))
@app.route('/clear-all-server', methods=['GET', 'POST'])
@@ -492,23 +640,37 @@ 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("")
for h in hosts:
try:
if h.type == 'linux' and linux_clear:
clear_linux(h, default_content)
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: {h.hostname}', 'success')
elif h.type == 'mikrotik' and mikrotik_clear:
clear_mikrotik(h)
flash(f'Cleared Mikrotik host: {h.hostname}', 'success')
except Exception as e:
flash(f'Error clearing host {h.hostname}: {str(e)}', 'danger')
return redirect(url_for('clear_all_server'))
return render_template('clear_servers.html', hosts=hosts)
return redirect(url_for('clear_all_server'))
return render_template('clear_servers.html', hosts=hosts)
# -------------------
# ZARZĄDZANIE PLIKAMI HOSTS (WIELOKROTNE PLIKI)
@@ -580,16 +742,39 @@ def deploy_hosts_file(file_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()
if request.method == 'POST':
selected_host_ids = request.form.getlist('hosts')
for host in hosts:
if str(host.id) in selected_host_ids:
try:
if host.type == 'linux':
# Przygotuj zawartość do wgrania
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}")
db.session.add(DeployLog(
details=f'[LINUX/DAEMON] Deployed file "{file.title}" to {host.hostname} for user {session["user_id"]}',
user_id=session['user_id']
))
elif host.type == 'mikrotik':
# Mikrotik
wrapped_mikro = wrap_mikrotik_content(file.content)
deploy_mikrotik(host, wrapped_mikro)
db.session.add(DeployLog(
details=f'[MIKROTIK] Deployed file "{file.title}" to {host.hostname} for user {session["user_id"]}',
user_id=session['user_id']
))
else:
# Standard Linux (SSH)
ssh = open_ssh_connection(host)
# Używamy file.content, nie hosts_content
adjusted_content = ensure_local_defaults(file.content)
wrapped_content = wrap_content_with_comments(adjusted_content)
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(wrapped_content)
tmp_file_path = tmpf.name
@@ -602,18 +787,13 @@ def deploy_hosts_file(file_id):
details=f'[LINUX] Deployed file "{file.title}" to {host.hostname} for user {session["user_id"]}',
user_id=session['user_id']
))
elif host.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(file.content)
deploy_mikrotik(host, wrapped_content)
db.session.add(DeployLog(
details=f'[MIKROTIK] Deployed file "{file.title}" to {host.hostname} for user {session["user_id"]}',
user_id=session['user_id']
))
db.session.commit()
flash(f'Deployed file "{file.title}" to {host.hostname}', 'success')
except Exception as e:
flash(f'Error deploying file "{file.title}" to {host.hostname}: {str(e)}', 'danger')
return redirect(url_for('list_hosts_files'))
return render_template('deploy_hosts_file.html', file=file, hosts=hosts)
# -------------------
@@ -628,13 +808,24 @@ def server_backup(host_id):
flash('Host not found or unauthorized', 'danger')
return redirect(url_for('server_list'))
try:
if host.type == 'mikrotik':
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 (daemon) from {host.hostname}'
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 (mikrotik) from {host.hostname}'
else:
# Standard Linux (SSH)
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with sftp.open('/etc/hosts', 'r') as remote_file:
@@ -642,6 +833,7 @@ def server_backup(host_id):
sftp.close()
ssh.close()
description = f'Backup from {host.hostname}'
backup = Backup(
user_id=session['user_id'],
host_id=host.id,
@@ -743,13 +935,24 @@ def backup_all():
hosts = Host.query.filter_by(user_id=user_id).all()
for host in hosts:
try:
if host.type == 'mikrotik':
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 (daemon) from {host.hostname}'
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 (mikrotik) from {host.hostname}'
else:
# Standard Linux (SSH)
ssh = open_ssh_connection(host)
sftp = ssh.open_sftp()
with sftp.open('/etc/hosts', 'r') as remote_file:
@@ -757,6 +960,7 @@ def backup_all():
sftp.close()
ssh.close()
description = f'Backup from {host.hostname}'
backup = Backup(
user_id=user_id,
host_id=host.id,
@@ -767,6 +971,7 @@ def backup_all():
db.session.commit()
except Exception as e:
flash(f'Error creating backup for host {host.hostname}: {str(e)}', 'danger')
flash('Backup for all hosts created successfully.', 'success')
return redirect(url_for('backups'))
@@ -934,25 +1139,59 @@ def delete_backup(backup_id):
# -------------------
def deploy_user(user_id):
user_settings = UserSettings.query.filter_by(user_id=user_id).first()
hostfile = HostFile.query.filter_by(user_id=user_id).first()
if not hostfile:
# domyślny plik "Default Hosts" (np. szukasz title=="Default Hosts")
default_file = HostFile.query.filter_by(user_id=user_id, title="Default Hosts").first()
if not default_file:
# jeśli nie ma w ogóle, nic nie robimy
return
hosts_content = hostfile.content
# ewentualnie regex
regex_lines = ""
if user_settings and user_settings.regex_deploy_enabled:
regex_lines = generate_regex_hosts(user_id)
else:
regex_lines = ""
final_content = regex_lines + hosts_content
hosts = Host.query.filter_by(user_id=user_id).all()
for h in hosts:
# Tylko dla serwerów z włączonym auto_deploy
if not h.auto_deploy_enabled:
continue
# Który plik wgrywać?
if h.preferred_hostfile_id:
chosen_file = HostFile.query.filter_by(id=h.preferred_hostfile_id, user_id=user_id).first()
if not chosen_file:
# fallback do default
chosen_file = default_file
else:
chosen_file = default_file
# final_content
final_content = regex_lines + chosen_file.content
try:
if h.type == 'linux':
if h.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(final_content)
deploy_mikrotik(h, wrapped_content)
db.session.add(DeployLog(
details=f'[MIKROTIK] Updated {h.hostname} for user {user_id}',
user_id=user_id
))
elif h.use_daemon and h.type == 'linux':
# Demon
import requests
adjusted_content = ensure_local_defaults(final_content)
wrapped_content = wrap_content_with_comments(adjusted_content)
url = h.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": h.daemon_token}
resp = requests.post(url, json={"hosts": wrapped_content}, headers=headers, timeout=10)
if resp.status_code != 200:
raise Exception(f"Daemon POST error: {resp.status_code} - {resp.text}")
db.session.add(DeployLog(
details=f'[LINUX/DAEMON] Updated {h.hostname} for user {user_id}',
user_id=user_id
))
else:
# standard linux - SSH
ssh = open_ssh_connection(h)
adjusted_content = ensure_local_defaults(final_content)
wrapped_content = wrap_content_with_comments(adjusted_content)
@@ -964,14 +1203,16 @@ def deploy_user(user_id):
sftp.close()
ssh.close()
os.remove(tmp_file_path)
db.session.add(DeployLog(details=f'[LINUX] Updated {h.hostname} for user {user_id}', user_id=user_id))
elif h.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(final_content)
deploy_mikrotik(h, wrapped_content)
db.session.add(DeployLog(details=f'[MIKROTIK] Updated {h.hostname} for user {user_id}', user_id=user_id))
db.session.add(DeployLog(
details=f'[LINUX] Updated {h.hostname} for user {user_id}',
user_id=user_id
))
db.session.commit()
except Exception as e:
db.session.add(DeployLog(details=f'Failed to update {h.hostname}: {str(e)} for user {user_id}'))
db.session.add(DeployLog(
details=f'Failed to update {h.hostname}: {str(e)} for user {user_id}'
))
db.session.commit()