fixy i usprwnienia

This commit is contained in:
Mateusz Gruszczyński 2025-03-06 10:38:12 +01:00
parent 4979365617
commit c4b753d4bd
6 changed files with 241 additions and 55 deletions

8
alters.txt Normal file
View File

@ -0,0 +1,8 @@
ALTER TABLE user_settings ADD COLUMN deploy_cron VARCHAR(100) DEFAULT '12 12 * * *';
ALTER TABLE user_settings ADD COLUMN backup_cron VARCHAR(100) DEFAULT '12 12 * * *';
ALTER TABLE host ADD COLUMN auto_deploy_enabled BOOLEAN DEFAULT 1;
ALTER TABLE host ADD COLUMN auto_backup_enabled BOOLEAN DEFAULT 1;
ALTER TABLE user_settings DROP COLUMN deploy_interval;
ALTER TABLE user_settings DROP COLUMN backup_interval;

144
app.py
View File

@ -7,7 +7,9 @@ from datetime import datetime, timezone, timedelta
from io import StringIO
import socket
import ipaddress
import pytz
from croniter import croniter
from tzlocal import get_localzone
from werkzeug.serving import WSGIRequestHandler
WSGIRequestHandler.server_version = ""
@ -38,7 +40,8 @@ class Host(db.Model):
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)
@property
def resolved_hostname(self):
try:
@ -63,8 +66,10 @@ 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_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)
@ -170,41 +175,43 @@ 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
)
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)}')
def automated_backups():
with app.app_context():
logger.debug("Rozpoczynam funkcję automated_backups")
hosts = Host.query.all()
now = datetime.now(timezone.utc)
hosts = Host.query.all()
for host in hosts:
settings = UserSettings.query.filter_by(user_id=host.user_id).first()
if not settings or not settings.auto_backup_enabled:
logger.debug(f"Pomijam host {host.hostname} - auto_backup nie włączone")
# Dodaj warunek: backup dla danego hosta ma być wykonywany tylko, jeśli jest włączony
if not host.auto_backup_enabled:
continue
backup_interval = settings.backup_interval if settings.backup_interval else 60
logger.debug(f"Backup interval dla hosta {host.hostname}: {backup_interval} minut")
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:
last_backup_time = last_backup.created_at
if last_backup_time.tzinfo is None:
# Zakładamy, że zapisany czas jest już w UTC
last_backup_time = last_backup_time.replace(tzinfo=timezone.utc)
diff = (now - last_backup_time).total_seconds()
logger.debug(f"Różnica czasu dla hosta {host.hostname}: {diff} sekund")
base_time = last_backup.created_at
if base_time.tzinfo is None:
base_time = base_time.replace(tzinfo=timezone.utc)
else:
last_backup_time = None
logger.debug(f"Brak poprzedniego backupu dla hosta {host.hostname}")
if (last_backup_time is None) or ((now - last_backup_time).total_seconds() >= backup_interval * 60):
logger.debug(f"Wykonuję backup dla hosta {host.hostname}")
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()
db.session.expire_all()
else:
logger.debug(f"Backup dla hosta {host.hostname} nie jest jeszcze potrzebny")
def wrap_content_with_comments(content):
@ -838,22 +845,28 @@ def settings():
if request.method == 'POST':
auto_deploy = request.form.get('auto_deploy')
deploy_interval = request.form.get('deploy_interval')
backup_interval = request.form.get('backup_interval')
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')
# 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)
try:
user_settings.deploy_interval = int(deploy_interval)
except ValueError:
user_settings.deploy_interval = 60
try:
user_settings.backup_interval = int(backup_interval)
except ValueError:
user_settings.backup_interval = 60
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)
@ -921,6 +934,9 @@ def deploy_user(user_id):
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
try:
if h.type == 'linux':
ssh = open_ssh_connection(h)
@ -934,12 +950,11 @@ 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))
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'[MIKROTIK] 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}'))
@ -1131,20 +1146,61 @@ def delete_regex_host(entry_id):
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
db.session.commit()
flash('Ustawienia automatyzacji zostały zaktualizowane.', 'success')
return redirect(url_for('server_list'))
def scheduled_deployments():
with app.app_context():
now = datetime.now(timezone.utc)
all_settings = UserSettings.query.filter_by(auto_deploy_enabled=True).all()
for setting in all_settings:
last_deploy_time = setting.last_deploy_time
if last_deploy_time:
last_deploy_time = last_deploy_time.replace(tzinfo=timezone.utc)
if not last_deploy_time or now - last_deploy_time >= timedelta(minutes=setting.deploy_interval):
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()
scheduler = BackgroundScheduler(timezone="UTC")
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())

View File

@ -5,9 +5,8 @@ from datetime import datetime
if __name__ == "__main__":
with app.app_context():
db.create_all()
for job in scheduler.get_jobs():
job.modify(next_run_time=datetime.now())
print(job)
if not scheduler.running:
scheduler.start()
serve(app, listen="*:5580", threads=4, ident="")
serve(app, listen="*:5580", threads=4, ident="")

View File

@ -19,6 +19,7 @@
<table class="table table-striped">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>Data utworzenia</th>
<th>Opis</th>
<th>Akcje</th>
@ -27,11 +28,16 @@
<tbody>
{% for backup in backups %}
<tr>
<td>
<!-- Każdy checkbox jest przypisany do formularza bulkDeleteForm -->
<input type="checkbox" name="selected_backups" value="{{ backup.id }}" form="bulkDeleteForm">
</td>
<td>{{ backup.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ backup.description }}</td>
<td>
<a href="{{ url_for('view_backup', backup_id=backup.id) }}" class="btn btn-sm btn-info">Podgląd</a>
<a href="{{ url_for('restore_backup', backup_id=backup.id) }}" class="btn btn-sm btn-success">Przywróć</a>
<!-- Usuwanie pojedynczego backupu -->
<form action="{{ url_for('delete_backup', backup_id=backup.id) }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć backup?');">Usuń</button>
</form>
@ -40,6 +46,22 @@
{% endfor %}
</tbody>
</table>
<!-- Formularz do bulk usuwania checkboxy znajdują się poza tym formularzem, ale dzięki atrybutowi form są z nim powiązane -->
<form id="bulkDeleteForm" action="{{ url_for('delete_selected_backups') }}" method="post">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć zaznaczone backupy?');">Usuń zaznaczone</button>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ super() }}
<script>
// Skrypt do zaznaczania/odznaczania wszystkich checkboxów
document.getElementById('select-all').addEventListener('change', function(){
var checkboxes = document.querySelectorAll('input[name="selected_backups"]');
checkboxes.forEach(function(checkbox) {
checkbox.checked = document.getElementById('select-all').checked;
});
});
</script>
{% endblock %}

View File

@ -24,6 +24,8 @@
<th>Port</th>
<th>Typ</th>
<th>Metoda uwierzytelniania</th>
<th>Auto Deploy</th>
<th>Auto Backup</th>
<th>Akcje</th>
</tr>
</thead>
@ -38,10 +40,24 @@
<td>{{ h.port }}</td>
<td>{{ h.type }}</td>
<td>{{ h.auth_method }}</td>
<!-- Formularz aktualizujący automatyczny deploy dla serwera -->
<td>
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" style="display:inline;">
<input type="hidden" name="setting" value="auto_deploy">
<input type="checkbox" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_deploy_enabled %}checked{% endif %}>
</form>
</td>
<!-- Formularz aktualizujący automatyczny backup dla serwera -->
<td>
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" style="display:inline;">
<input type="hidden" name="setting" value="auto_backup">
<input type="checkbox" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_backup_enabled %}checked{% endif %}>
</form>
</td>
<td>
<a href="{{ url_for('edit_server', id=h.id) }}" class="btn btn-primary btn-sm">Edytuj</a>
<a href="{{ url_for('test_server_connection', id=h.id) }}" class="btn btn-info btn-sm">Testuj</a>
<a href="{{ url_for('server_backup', host_id=h.id) }}" class="btn btn-success btn-sm">Backup</a>
<a href="{{ url_for('server_backup', host_id=h.id) }}" class="btn btn-success btn-sm">Backup</a>
<form method="GET" action="{{ url_for('delete_server', id=h.id) }}" style="display:inline;">
<button type="submit" class="btn btn-danger btn-sm">Usuń</button>
</form>

View File

@ -21,17 +21,25 @@
<label class="form-check-label" for="auto_deploy">Automatyczny deploy</label>
</div>
<div class="mb-3">
<label for="deploy_interval" class="form-label">Interwał deploy (minuty)</label>
<input type="number" class="form-control" id="deploy_interval" name="deploy_interval" value="{{ settings.deploy_interval }}">
</div>
<div class="mb-3">
<label for="backup_interval" class="form-label">Interwał backupów (minuty)</label>
<input type="number" class="form-control" id="backup_interval" name="backup_interval" value="{{ settings.backup_interval }}">
<label for="deploy_cron" class="form-label">Harmonogram deploy (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="deploy_cron" name="deploy_cron" value="{{ settings.deploy_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('deploy_cron')">Generuj cron</button>
</div>
<small class="text-muted">Np. <code>0 0 * * *</code> codziennie o północy</small>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="auto_backup" name="auto_backup" {% if settings.auto_backup_enabled %}checked{% endif %}>
<label class="form-check-label" for="auto_backup">Automatyczne kopie zapasowe</label>
</div>
<div class="mb-3">
<label for="backup_cron" class="form-label">Harmonogram backup (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="backup_cron" name="backup_cron" value="{{ settings.backup_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('backup_cron')">Generuj cron</button>
</div>
<small class="text-muted">Np. <code>0 */6 * * *</code> co 6 godzin</small>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enable_regex_entries" name="enable_regex_entries" {% if settings.regex_deploy_enabled %}checked{% endif %}>
<label class="form-check-label" for="enable_regex_entries">Włącz regex/CIDR deploy</label>
@ -44,4 +52,81 @@
</form>
</div>
</div>
<!-- Modal do generowania wyrażenia CRON -->
<div class="modal fade" id="cronModal" tabindex="-1" aria-labelledby="cronModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cronModalLabel">Generuj wyrażenie CRON</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="cron_minute" class="form-label">Minuta</label>
<input type="text" class="form-control" id="cron_minute" placeholder="0-59">
</div>
<div class="mb-3">
<label for="cron_hour_modal" class="form-label">Godzina</label>
<input type="text" class="form-control" id="cron_hour_modal" placeholder="0-23">
</div>
<div class="mb-3">
<label for="cron_day" class="form-label">Dzień miesiąca</label>
<input type="text" class="form-control" id="cron_day" placeholder="1-31 lub *">
</div>
<div class="mb-3">
<label for="cron_month" class="form-label">Miesiąc</label>
<input type="text" class="form-control" id="cron_month" placeholder="1-12 lub *">
</div>
<div class="mb-3">
<label for="cron_dow" class="form-label">Dzień tygodnia</label>
<input type="text" class="form-control" id="cron_dow" placeholder="0-6 (0 = niedziela) lub *">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="button" class="btn btn-primary" onclick="generateCronExpression()">Generuj</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ super() }}
<script>
// Globalna zmienna, która będzie przechowywać ID pola formularza do aktualizacji
var currentCronField = null;
function openCronModal(fieldId) {
currentCronField = fieldId;
// Resetuj wartości w modal
document.getElementById('cron_minute').value = "";
document.getElementById('cron_hour_modal').value = "";
document.getElementById('cron_day').value = "";
document.getElementById('cron_month').value = "";
document.getElementById('cron_dow').value = "";
// Wyświetl modal przy użyciu Bootstrap
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
cronModal.show();
}
function generateCronExpression() {
var minute = document.getElementById('cron_minute').value || "*";
var hour = document.getElementById('cron_hour_modal').value || "*";
var day = document.getElementById('cron_day').value || "*";
var month = document.getElementById('cron_month').value || "*";
var dow = document.getElementById('cron_dow').value || "*";
var cronExpression = minute + " " + hour + " " + day + " " + month + " " + dow;
if (currentCronField) {
document.getElementById(currentCronField).value = cronExpression;
}
// Zamknij modal
var modalEl = document.getElementById('cronModal');
var modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
}
</script>
{% endblock %}