duzo fixow i optymalizacji

This commit is contained in:
Mateusz Gruszczyński 2025-03-10 23:32:43 +01:00
parent c4b5e703f3
commit a8954020f6
4 changed files with 274 additions and 106 deletions

128
app.py
View File

@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, Response
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
@ -159,7 +159,11 @@ def ensure_local_defaults(content, user_id):
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
@ -175,12 +179,8 @@ def format_host(host):
except (socket.herror, socket.gaierror):
pass # Jeśli nie można rozwiązać, pozostaw IP
return f"{resolved_name} ({host.raw_ip})"
return f"{resolved_name} ({host.raw_ip})" if resolved_name else f"Unknown ({host.raw_ip})"
# Automatyczne dodanie wartości systemowych
variables["hostname"] = socket.gethostname() # Nazwa hosta
variables["resolved_hostname"] = socket.getfqdn() # Pełna nazwa hosta
return variables
def wrap_content_with_comments(content):
@ -679,48 +679,54 @@ def clear_server():
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_servers'))
return redirect(url_for('clear_server'))
default_content = ensure_local_defaults("", session['user_id'])
default_content = ensure_local_defaults("")
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}
# 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)
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')
flash(f'Cleared host: {host_name}', 'success')
except Exception as e:
flash(f'Error clearing host {host.hostname}: {str(e)}', 'danger')
flash(f'Error clearing host {host_name}: {str(e)}', 'danger')
return redirect(url_for('clear_server'))
return redirect(url_for('clear_all_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("")
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
@ -732,18 +738,18 @@ def clear_all_server():
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')
flash(f'Cleared Linux host: {host_name}', 'success')
elif h.type == 'mikrotik' and mikrotik_clear:
clear_mikrotik(h)
flash(f'Cleared Mikrotik host: {h.hostname}', 'success')
flash(f'Cleared Mikrotik host: {host_name}', 'success')
except Exception as e:
flash(f'Error clearing host {h.hostname}: {str(e)}', 'danger')
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)
return render_template('clear_servers.html', hosts=hosts, format_host=format_host)
# -------------------
# ZARZĄDZANIE PLIKAMI HOSTS (WIELOKROTNE PLIKI)
@ -1246,35 +1252,24 @@ def deploy_user(user_id):
if not h.auto_deploy_enabled:
continue
# Pobranie pliku hosts wybranego dla serwera
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:
chosen_file = default_file
else:
chosen_file = default_file
# Wstępna zawartość - pobrany plik hosts
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()
# Jeśli disable_local_default jest włączone → usuwamy local-defaults, ale reszta pozostaje
if h.disable_local_default:
final_content = remove_local_defaults(final_content, user_id)
else:
# Dodaj regex, jeśli nie jest wyłączony
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:
# 🖥 Wgrywanie na Mikrotik
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 {format_host(h)} for user {user_id}'
log_details = f'[MIKROTIK] Updated {host_name} for user {user_id}'
# 🖥 Wgrywanie na Linux Daemon
elif h.use_daemon and h.type == 'linux':
import requests
wrapped_content = wrap_content_with_comments(final_content)
@ -1283,9 +1278,8 @@ def deploy_user(user_id):
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(h)} for user {user_id}'
log_details = f'[LINUX/DAEMON] Updated {host_name} for user {user_id}'
# 🖥 Wgrywanie na standardowy Linux przez SSH
else:
ssh = open_ssh_connection(h)
wrapped_content = wrap_content_with_comments(final_content)
@ -1297,14 +1291,13 @@ def deploy_user(user_id):
sftp.close()
ssh.close()
os.remove(tmp_file_path)
log_details = f'[LINUX] Updated {format_host(h)} for user {user_id}'
log_details = f'[LINUX] Updated {host_name} for user {user_id}'
# Logowanie wdrożenia
db.session.add(DeployLog(details=log_details, user_id=user_id))
db.session.commit()
except Exception as e:
error_log = f'Failed to update {format_host(h)}: {str(e)} for user {user_id}'
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()
@ -1751,7 +1744,9 @@ def local_defaults():
ip_address = request.form.get('ip_address', '').strip()
if hostname and ip_address:
new_entry = LocalDefaultEntry(user_id=user_id, hostname=hostname, ip_address=ip_address or None, entry=entry_content)
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')
@ -1764,10 +1759,34 @@ def local_defaults():
return render_template('local_defaults.html', entries=entries)
@app.route('/local-defaults/delete/<int:entry_id>', methods=['POST'])
def delete_local_default(entry_id):
@app.route('/local-defaults/delete', methods=['POST'])
def delete_local_default(entry_id=None):
if 'user_id' not in session:
return redirect(url_for('login'))
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')
@ -1778,6 +1797,29 @@ def delete_local_default(entry_id):
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())

View File

@ -40,10 +40,6 @@
<li><a class="dropdown-item" href="{{ url_for('import_servers') }}">Importuj serwery z CSV</a></li>
</ul>
</li>
<!-- WYczysc /etc/hosts -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('clear_server') }}">Wyczyść /etc/hosts</a>
</li>
<!-- Edytuj /etc/hosts z podsekcjami -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="editHostsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
@ -75,6 +71,17 @@
<li><a class="dropdown-item" href="{{ url_for('new_hosts_file') }}">Utwórz nowy /etc/hosts</a></li>
</ul>
</li>
<!-- Dpmyślne wpisy -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('local_defaults') }}">Domyślne /etc/hosts</a>
</li>
<!-- Wyczysc /etc/hosts -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('clear_server') }}">Wyczyść /etc/hosts</a>
</li>
<!-- Kopie zapasowe -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="backupsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
@ -85,17 +92,6 @@
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="filesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Domyślne wpisy
</a>
<ul class="dropdown-menu" aria-labelledby="filesDropdown">
<li><a class="nav-link" href="{{ url_for('local_defaults') }}">Domyślne /etc/hosts</a></li>
</ul>
</li>
<!-- Ustawienia -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('settings') }}">Ustawienia</a>

View File

@ -61,7 +61,6 @@
{% block extra_js %}
{{ super() }}
<script>
// Ustaw dynamicznie action formularza dla czyszczenia pojedynczego serwera
document.getElementById('clear-single-form').addEventListener('submit', function(e) {
e.preventDefault();
var hostId = document.getElementById('host_id').value;
@ -69,11 +68,9 @@
alert("Proszę wybrać serwer!");
return;
}
// Skonstruuj URL bez użycia url_for
this.action = "/clear-single-server/" + hostId;
this.submit();
});
</script>
{% endblock %}

View File

@ -4,26 +4,35 @@
{% block content %}
<div class="card">
<div class="card-header">
<h3>Ustaw domyślne wpisy /etc/hosts z dynamicznymi zmiennymi</h3>
<h3>Ustaw domyślne wpisy /etc/hosts</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-6">
<div class="row align-items-end">
<div class="col-md-4">
<label class="form-label">Adres IP</label>
<input type="text" class="form-control" name="ip_address" placeholder="np. 127.0.0.1" required>
</div>
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">Hostname</label>
<input type="text" class="form-control" name="hostname" placeholder="np. localhost" required>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">Dodaj wpis</button>
</div>
</div>
<button type="submit" class="btn btn-primary mt-3">Dodaj wpis</button>
</form>
<hr>
<form id="bulkDeleteForm">
<div class="mb-2">
<button type="button" class="btn btn-danger btn-sm" id="deleteSelectedBtn" disabled>Usuń zaznaczone</button>
</div>
<table class="table table-striped mt-4">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"></th>
<th>ID</th>
<th>Adres IP</th>
<th>Hostname</th>
@ -33,11 +42,15 @@
<tbody>
{% for e in entries %}
<tr>
<td><input type="checkbox" class="entry-checkbox" value="{{ e.id }}"></td>
<td>{{ e.id }}</td>
<td>{{ e.ip_address }}</td>
<td>{{ e.hostname }}</td>
<td class="ip-address">{{ e.ip_address }}</td>
<td class="hostname">{{ e.hostname }}</td>
<td>
<form method="POST" action="{{ url_for('delete_local_default', entry_id=e.id) }}" onsubmit="return confirm('Usunąć wpis?');">
<button class="btn btn-warning btn-sm edit-btn" data-id="{{ e.id }}">Edytuj</button>
<button class="btn btn-success btn-sm save-btn d-none" data-id="{{ e.id }}">Zapisz</button>
<form method="POST" action="{{ url_for('delete_local_default', entry_id=e.id) }}"
onsubmit="return confirm('Usunąć wpis?');" style="display:inline;">
<button class="btn btn-danger btn-sm">Usuń</button>
</form>
</td>
@ -47,6 +60,126 @@
{% endfor %}
</tbody>
</table>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ super() }}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Logika edycji w tabeli
document.querySelectorAll(".edit-btn").forEach(button => {
button.addEventListener("click", function() {
let row = this.closest("tr");
let entryId = this.getAttribute("data-id");
let ipCell = row.querySelector(".ip-address");
let hostnameCell = row.querySelector(".hostname");
let saveButton = row.querySelector(".save-btn");
let ipInput = document.createElement("input");
ipInput.type = "text";
ipInput.className = "form-control form-control-sm";
ipInput.value = ipCell.textContent.trim();
let hostnameInput = document.createElement("input");
hostnameInput.type = "text";
hostnameInput.className = "form-control form-control-sm";
hostnameInput.value = hostnameCell.textContent.trim();
ipCell.textContent = "";
hostnameCell.textContent = "";
ipCell.appendChild(ipInput);
hostnameCell.appendChild(hostnameInput);
this.classList.add("d-none");
saveButton.classList.remove("d-none");
saveButton.addEventListener("click", function() {
let newIp = ipInput.value.trim();
let newHostname = hostnameInput.value.trim();
fetch(`/local-defaults/update/${entryId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ip_address: newIp, hostname: newHostname })
})
.then(response => response.json())
.then(data => {
if (data.status === "success") {
ipCell.textContent = newIp;
hostnameCell.textContent = newHostname;
button.classList.remove("d-none");
saveButton.classList.add("d-none");
} else {
alert("Błąd: " + data.message);
}
})
.catch(error => {
alert("Wystąpił błąd podczas zapisywania: " + error);
});
});
});
});
// Logika zaznaczania i usuwania wpisów
let deleteButton = document.getElementById("deleteSelectedBtn");
let checkboxes = document.querySelectorAll(".entry-checkbox");
let selectAllCheckbox = document.getElementById("selectAll");
function updateDeleteButtonState() {
let anyChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
deleteButton.disabled = !anyChecked;
}
checkboxes.forEach(checkbox => {
checkbox.addEventListener("change", updateDeleteButtonState);
});
selectAllCheckbox.addEventListener("change", function() {
let isChecked = this.checked;
checkboxes.forEach(checkbox => checkbox.checked = isChecked);
updateDeleteButtonState();
});
deleteButton.addEventListener("click", function() {
let selectedIds = Array.from(checkboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
if (selectedIds.length === 0) {
alert("Nie zaznaczono żadnych wpisów do usunięcia.");
return;
}
if (!confirm("Czy na pewno chcesz usunąć zaznaczone wpisy?")) {
return;
}
fetch("/local-defaults/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entry_ids: selectedIds })
})
.then(response => response.json())
.then(data => {
if (data.status === "success") {
selectedIds.forEach(id => {
let row = document.querySelector(`input[value='${id}']`).closest("tr");
row.remove();
});
updateDeleteButtonState();
} else {
alert("Błąd: " + data.message);
}
})
.catch(error => {
alert("Wystąpił błąd podczas usuwania: " + error);
});
});
});
</script>
{% endblock %}