nowa funkcja / domyslne wpisy

This commit is contained in:
Mateusz Gruszczyński 2025-03-10 12:04:22 +01:00
parent 7fb796357f
commit 9cffcb0ca6
7 changed files with 363 additions and 82 deletions

View File

@ -29,3 +29,5 @@ ALTER TABLE user_settings ADD COLUMN global_ssh_key TEXT;
ALTER TABLE user_settings ADD COLUMN global_key_passphrase VARCHAR(200);
ALTER TABLE host ADD COLUMN disable_regex_deploy BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE host ADD COLUMN disable_local_default BOOLEAN DEFAULT FALSE;

149
app.py
View File

@ -48,6 +48,7 @@ class Host(db.Model):
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):
@ -81,11 +82,13 @@ class DeployLog(db.Model):
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)
@ -100,6 +103,7 @@ class UserSettings(db.Model):
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)
@ -128,18 +132,60 @@ class HostFileVersion(db.Model):
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)
dynamic_variable = db.Column(db.String(255), nullable=True)
entry = db.Column(db.Text, nullable=False) # To pole już istnieje
user = db.relationship('User', backref='local_defaults')
def formatted_entry(self, variables={}):
entry_content = f"{self.ip_address} {self.hostname}"
if self.dynamic_variable:
dynamic_content = self.dynamic_variable
for key, value in variables.items():
placeholder = f"${{{key}}}"
dynamic_content = dynamic_content.replace(placeholder, value)
entry_content += f" {dynamic_content}"
return entry_content
class UserDynamicVariables(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
variable_name = db.Column(db.String(255), nullable=False)
variable_value = db.Column(db.String(255), nullable=False)
user = db.relationship('User', backref='dynamic_variables')
# Funkcje pomocnicze
def ensure_local_defaults(content):
required_lines = [
"127.0.0.1 localhost",
"::1 localhost ip6-localhost ip6-loopback",
"255.255.255.255 broadcasthost"
]
def get_user_dynamic_variables(user_id):
user_variables = UserDynamicVariables.query.filter_by(user_id=user_id).all()
return {var.variable_name: var.variable_value for var in user_variables}
def ensure_local_defaults(content, user_id):
default_entries = LocalDefaultEntry.query.filter_by(user_id=user_id).all()
required_lines = [entry.hostname for entry in default_entries]
lines = [l.rstrip() for l in content.splitlines()]
lines = [line for line in lines if line not in required_lines]
lines = required_lines + lines
return "\n".join(lines) + "\n"
def get_user_dynamic_variables(user_id):
user_variables = UserDynamicVariables.query.filter_by(user_id=user_id).all()
variables = {var.variable_name: var.variable_value for var in user_variables}
# 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):
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
header_comment = f"# Auto-hosts upload: {now_str}\n"
@ -1193,7 +1239,11 @@ def deploy_user(user_id):
chosen_file = default_file
else:
chosen_file = default_file
final_content = ("" if h.disable_regex_deploy else regex_lines) + chosen_file.content
# Nowa logika: jeśli `disable_local_default` jest włączone, pomijamy lokalne ustawienia
final_content = ("" if h.disable_regex_deploy else regex_lines) + \
("" if h.disable_local_default else ensure_local_defaults(chosen_file.content))
try:
if h.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(final_content)
@ -1202,7 +1252,7 @@ def deploy_user(user_id):
db.session.add(DeployLog(details=log_details, user_id=user_id))
elif h.use_daemon and h.type == 'linux':
import requests
adjusted_content = ensure_local_defaults(final_content)
adjusted_content = ensure_local_defaults(final_content) if not h.disable_local_default else final_content
wrapped_content = wrap_content_with_comments(adjusted_content)
url = h.daemon_url.rstrip('/') + '/hosts'
headers = {"Authorization": h.daemon_token}
@ -1213,7 +1263,7 @@ def deploy_user(user_id):
db.session.add(DeployLog(details=log_details, user_id=user_id))
else:
ssh = open_ssh_connection(h)
adjusted_content = ensure_local_defaults(final_content)
adjusted_content = ensure_local_defaults(final_content) if not h.disable_local_default else final_content
wrapped_content = wrap_content_with_comments(adjusted_content)
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpf:
tmpf.write(wrapped_content)
@ -1230,7 +1280,6 @@ def deploy_user(user_id):
db.session.add(DeployLog(details=f'Failed to update {format_host(h)}: {str(e)} for user {user_id}', 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")
@ -1447,6 +1496,8 @@ def update_host_automation(id):
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'))
@ -1659,6 +1710,84 @@ 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()
dynamic_variable = request.form.get('dynamic_variable', '').strip()
if hostname and ip_address:
entry_content = f"{ip_address} {hostname} {dynamic_variable}".strip()
new_entry = LocalDefaultEntry(user_id=user_id, hostname=hostname, ip_address=ip_address, dynamic_variable=dynamic_variable or None, 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'])
def delete_local_default(entry_id):
if 'user_id' not in session:
return redirect(url_for('login'))
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('/dynamic-variables', methods=['GET', 'POST'])
def dynamic_variables():
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
if request.method == 'POST':
variable_name = request.form.get('variable_name', '').strip()
variable_value = request.form.get('variable_value', '').strip()
if variable_name and variable_value:
new_variable = UserDynamicVariables(user_id=user_id, variable_name=variable_name, variable_value=variable_value)
db.session.add(new_variable)
db.session.commit()
flash('Dodano nową zmienną dynamiczną.', 'success')
else:
flash('Nazwa i wartość zmiennej są wymagane.', 'danger')
return redirect(url_for('dynamic_variables'))
variables = UserDynamicVariables.query.filter_by(user_id=user_id).all()
return render_template('dynamic_variables.html', variables=variables)
@app.route('/dynamic-variables/delete/<int:variable_id>', methods=['POST'])
def delete_dynamic_variable(variable_id):
if 'user_id' not in session:
return redirect(url_for('login'))
variable = UserDynamicVariables.query.get(variable_id)
if not variable or variable.user_id != session['user_id']:
flash('Nie znaleziono zmiennej lub brak uprawnień.', 'danger')
return redirect(url_for('dynamic_variables'))
db.session.delete(variable)
db.session.commit()
flash('Zmienna została usunięta.', 'info')
return redirect(url_for('dynamic_variables'))
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

@ -1,71 +1,82 @@
/* Style trybu ciemnego stosujemy je tylko, gdy body ma klasę dark-mode */
body.dark-mode {
background-color: #121212;
color: #e0e0e0;
}
body.dark-mode footer {
background-color: #1e1e1e !important;
}
/* Tabele style ciemnego motywu */
body.dark-mode .table {
color: #e0e0e0;
background-color: #1e1e1e;
border: 1px solid #444;
}
body.dark-mode .table th,
body.dark-mode .table td {
border: 1px solid #444;
}
body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
background-color: #2e2e2e;
}
body.dark-mode .table-striped tbody tr:nth-of-type(even) {
background-color: #1e1e1e;
}
body.dark-mode .table thead {
background-color: #333;
color: #e0e0e0;
}
/* Karty */
body.dark-mode .card {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
/* Formularze */
body.dark-mode .form-control,
body.dark-mode .form-select {
background-color: #2e2e2e;
color: #e0e0e0;
border: 1px solid #444;
}
body.dark-mode .form-control:focus,
body.dark-mode .form-select:focus {
background-color: #2e2e2e;
color: #e0e0e0;
border-color: #777;
box-shadow: none;
}
/* Przycisk Wyloguj solidny przycisk, by był czytelny */
.btn-logout {
color: #fff;
}
/* Zmniejszenie rozmiaru czcionki w navbarze */
.navbar {
font-size: 0.9rem; /* zmniejszony rozmiar czcionki */
}
/* Style trybu ciemnego stosujemy je tylko, gdy body ma klasę dark-mode */
body.dark-mode {
background-color: #121212;
color: #e0e0e0;
}
body.dark-mode footer {
background-color: #1e1e1e !important;
}
/* Tabele style ciemnego motywu */
body.dark-mode .table {
color: #e0e0e0;
background-color: #1e1e1e;
border: 1px solid #444;
}
body.dark-mode .table th,
body.dark-mode .table td {
border: 1px solid #444;
}
body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
background-color: #2e2e2e;
}
body.dark-mode .table-striped tbody tr:nth-of-type(even) {
background-color: #1e1e1e;
}
body.dark-mode .table thead {
background-color: #333;
color: #e0e0e0;
}
/* Karty */
body.dark-mode .card {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
/* Formularze */
body.dark-mode .form-control,
body.dark-mode .form-select {
background-color: #2e2e2e;
color: #e0e0e0;
border: 1px solid #444;
}
body.dark-mode .form-control:focus,
body.dark-mode .form-select:focus {
background-color: #2e2e2e;
color: #e0e0e0;
border-color: #777;
box-shadow: none;
}
/* Przycisk Wyloguj solidny przycisk, by był czytelny */
.btn-logout {
color: #fff;
}
/* Sprytne odwracanie kolorow dla svg */
html[data-bs-theme="dark"] .mikrotik-logo {
filter: invert(1);
}
html[data-bs-theme="dark"] .linux-logo {
filter: invert(1);
}
/* Zmniejszenie rozmiaru czcionki w navbarze */
.navbar {
font-size: 0.85rem; /* zmniejszony rozmiar czcionki */
}
.progress-bar {
display: flex;
justify-content: center;
align-items: center;
}
/* Sprytne odwracanie kolorow dla svg */
html[data-bs-theme="dark"] .mikrotik-logo {
filter: invert(1);
}
html[data-bs-theme="dark"] .linux-logo {
filter: invert(1);
}
.progress-bar {
display: flex;
justify-content: center;
align-items: center;
}
/* Zmniejszenie tekstu w rozwijanym menu navbar */
.navbar .dropdown-menu {
font-size: 0.72rem; /* Dostosuj rozmiar czcionki */
}
/* Zmniejszenie paddingu dla elementów dropdown */
.navbar .dropdown-item {
font-size: 0.72rem; /* Dostosuj rozmiar czcionki */
padding: 0.1rem 0.5rem; /* Mniejszy padding */
}

View File

@ -68,7 +68,7 @@
<!-- Pliki /etc/hosts -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="filesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Pliki /etc/hosts (beta)
Pliki /etc/hosts
</a>
<ul class="dropdown-menu" aria-labelledby="filesDropdown">
<li><a class="dropdown-item" href="{{ url_for('list_hosts_files') }}">Lista plików</a></li>
@ -84,6 +84,18 @@
<li><a class="dropdown-item" href="{{ url_for('backups') }}">Lista kopii</a></li>
</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>
<li><a class="nav-link" href="{{ url_for('dynamic_variables') }}">Dynamiczne zmienne</a></li>
</ul>
</li>
<!-- Ustawienia -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('settings') }}">Ustawienia</a>

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Zarządzanie zmiennymi dynamicznymi{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h3>Definiowanie zmiennych dynamicznych</h3>
<p class="text-muted">Te zmienne mogą być używane w konfiguracji <code>/etc/hosts</code>, np. <code>${app_name}</code>, <code>${hostname}</code>.</p>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-5">
<label class="form-label">Nazwa zmiennej</label>
<input type="text" class="form-control" name="variable_name" placeholder="np. app_name" required>
</div>
<div class="col-md-5">
<label class="form-label">Wartość zmiennej</label>
<input type="text" class="form-control" name="variable_value" placeholder="np. MojaAplikacja" required>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Dodaj</button>
</div>
</div>
</form>
<table class="table table-striped mt-4">
<thead>
<tr>
<th>ID</th>
<th>Nazwa zmiennej</th>
<th>Wartość</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for var in variables %}
<tr>
<td>{{ var.id }}</td>
<td><code>${{ var.variable_name }}</code></td>
<td>{{ var.variable_value }}</td>
<td>
<form method="POST" action="{{ url_for('delete_dynamic_variable', variable_id=var.id) }}" onsubmit="return confirm('Usunąć zmienną?');">
<button class="btn btn-danger btn-sm">Usuń</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="4">Brak zdefiniowanych zmiennych.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}Definiowanie wpisów z dynamicznymi zmiennymi{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h3>Ustaw domyślne wpisy /etc/hosts z dynamicznymi zmiennymi</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<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-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">
<label class="form-label">Dynamiczna zmienna (opcjonalnie)</label>
<input type="text" class="form-control" name="dynamic_variable" placeholder="np. ${app_name}">
<small class="form-text">Dostępne zmienne: ${resolved_hostname}, ${hostname}, ${app_name}, ${function}, ${user}</small>
</div>
</div>
<button type="submit" class="btn btn-primary mt-3">Dodaj wpis</button>
</form>
<table class="table table-striped mt-4">
<thead>
<tr>
<th>ID</th>
<th>Adres IP</th>
<th>Hostname</th>
<th>Zmienna dynamiczna</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for e in entries %}
<tr>
<td>{{ e.id }}</td>
<td>{{ e.ip_address }}</td>
<td>{{ e.hostname }}</td>
<td>{{ e.dynamic_variable or '—' }}</td>
<td>
<form method="POST" action="{{ url_for('delete_local_default', entry_id=e.id) }}" onsubmit="return confirm('Usunąć wpis?');">
<button class="btn btn-danger btn-sm">Usuń</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="5">Brak zdefiniowanych wpisów.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -50,7 +50,8 @@
<th>Wybrany /etc/hosts</th>
<th>Auto Deploy</th>
<th>Auto Backup</th>
<th>Wyłącz CIDR / regex</th>
<th>Wyłącz <a href="{{ url_for('list_regex_hosts') }}">CIDR / regex</a></th>
<th>Wyłącz <a href="{{ url_for('local_defaults') }}">local-defaults</a></th>
<th>Akcje</th>
</tr>
</thead>
@ -152,7 +153,17 @@
</div>
</form>
</td>
<td>
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" class="d-flex justify-content-center">
<input type="hidden" name="setting" value="disable_local_default">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="disableLocalDefaultCheckbox{{ h.id }}" name="enabled" value="1" onchange="this.form.submit()" {% if h.disable_local_default %}checked{% endif %}>
<label class="form-check-label" for="disableLocalDefaultCheckbox{{ h.id }}"></label>
</div>
</form>
</td>
<td>
<div class="d-flex flex-wrap gap-1">
<a href="{{ url_for('edit_server', id=h.id) }}" class="btn btn-primary btn-sm btn-action">Edytuj</a>