diff --git a/alters.txt b/alters.txt new file mode 100644 index 0000000..5b1eace --- /dev/null +++ b/alters.txt @@ -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; diff --git a/app.py b/app.py index 508e0f1..5893db2 100644 --- a/app.py +++ b/app.py @@ -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()) diff --git a/run_waitress.py b/run_waitress.py index 0ee98e4..490d2ec 100644 --- a/run_waitress.py +++ b/run_waitress.py @@ -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="") \ No newline at end of file diff --git a/templates/backups.html b/templates/backups.html index 73337a7..b8dd1c4 100644 --- a/templates/backups.html +++ b/templates/backups.html @@ -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 %} diff --git a/templates/server_list.html b/templates/server_list.html index 796720d..4e526fb 100644 --- a/templates/server_list.html +++ b/templates/server_list.html @@ -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> diff --git a/templates/settings.html b/templates/settings.html index 8fd6d05..f8d5cae 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -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 %} +