From 92e3049a8ecfa44e0df044342c70f07e58b40d66 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 30 Dec 2025 11:30:55 -0300 Subject: [PATCH] Add cluster API for worker management and status reporting --- cluster/cluster_api.py | 152 ++++++++++++++++++ .../migrations/0006_worker_error_status.py | 18 +++ .../0007_workerstatus_worker_version.py | 18 +++ .../0008_alter_worker_error_status.py | 18 +++ .../0009_alter_worker_error_status.py | 18 +++ ...0010_alter_worker_error_status_and_more.py | 23 +++ cluster/models.py | 11 ++ cluster/views.py | 2 +- templates/cluster/workers_list.html | 13 +- wireguard_webadmin/urls.py | 6 +- 10 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 cluster/cluster_api.py create mode 100644 cluster/migrations/0006_worker_error_status.py create mode 100644 cluster/migrations/0007_workerstatus_worker_version.py create mode 100644 cluster/migrations/0008_alter_worker_error_status.py create mode 100644 cluster/migrations/0009_alter_worker_error_status.py create mode 100644 cluster/migrations/0010_alter_worker_error_status_and_more.py diff --git a/cluster/cluster_api.py b/cluster/cluster_api.py new file mode 100644 index 0000000..4bd3770 --- /dev/null +++ b/cluster/cluster_api.py @@ -0,0 +1,152 @@ +import glob +import os + +from django.http import JsonResponse +from django.utils import timezone + +from .models import ClusterSettings, Worker, WorkerStatus + + +def get_ip_address(request): + ip_address = request.META.get('HTTP_X_FORWARDED_FOR') + if ip_address: + ip_address = ip_address.split(',')[0] + else: + ip_address = request.META.get('REMOTE_ADDR') + return ip_address + + +def get_worker(request): + min_worker_version = 1 + success = True + ip_address = get_ip_address(request) + token = request.GET.get('token', '') + try: + worker = Worker.objects.get(token=token) + except: + return None, False + + worker_status, created = WorkerStatus.objects.get_or_create(worker=worker) + try: + worker_config_version = int(request.GET.get('worker_config_version')) + worker_version = int(request.GET.get('worker_version')) + except: + worker.error_status = 'missing_version' + worker.save() + return worker, False + + if worker.error_status == 'missing_version': + worker.error_status = '' + worker.save() + + if worker_version < min_worker_version: + worker.error_status = 'update_required' + worker.save() + return worker, False + if worker.error_status == 'update_required': + worker.error_status = '' + worker.save() + + if worker_status.config_version != worker_config_version: + worker_status.config_version = worker_config_version + if worker_status.worker_version != worker_version: + worker_status.worker_version = worker_version + worker_status.last_seen = timezone.now() + worker_status.save() + + if not worker.ip_address: + worker.ip_address = ip_address + worker.save() + + if worker.ip_lock: + if worker.ip_address == ip_address: + if worker.error_status == 'ip_lock': + worker.error_status = '' + worker.save() + else: + worker.error_status = 'ip_lock' + worker.save() + success = False + else: + if worker.ip_address != ip_address: + worker.ip_address = ip_address + worker.save() + + if worker.enabled: + if worker.error_status == 'worker_disabled': + worker.error_status = '' + worker.save() + else: + worker.error_status = 'worker_disabled' + worker.save() + success = False + + cluster_settings, created = ClusterSettings.objects.get_or_create(name='cluster_settings') + if cluster_settings.enabled: + if worker.error_status == 'cluster_disabled': + worker.error_status = '' + worker.save() + else: + worker.error_status = 'cluster_disabled' + worker.save() + success = False + + return worker, success + + +def api_get_worker_config_files(request): + worker, success = get_worker(request) + if worker: + if worker.error_status or not success: + data = {'status': 'error', 'message': worker.error_status} + return JsonResponse(data, status=400) + else: + data = {'status': 'error', 'message': 'Worker not found'} + return JsonResponse(data, status=403) + + config_files = ( + glob.glob('/etc/wireguard/wg*.conf') + + glob.glob('/etc/wireguard/wg-firewall.sh') + ) + + files = {} + + for path in config_files: + filename = os.path.basename(path) + with open(path, 'r') as f: + files[filename] = f.read() + + return JsonResponse( + { + 'status': 'success', + 'files': files, + }, + status=200 + ) + + +def api_cluster_status(request): + worker, success = get_worker(request) + if worker: + if worker.error_status or not success: + data = {'status': 'error', 'message': worker.error_status} + return JsonResponse(data, status=400) + else: + data = {'status': 'error', 'message': 'Worker not found'} + return JsonResponse(data, status=403) + cluster_settings, created = ClusterSettings.objects.get_or_create(name='cluster_settings') + data = { + 'status': 'success', + 'worker_error_status': worker.error_status, + 'cluster_settings': { + 'enabled': cluster_settings.enabled, + 'primary_enable_wireguard': cluster_settings.primary_enable_wireguard, + 'stats_sync_interval': cluster_settings.stats_sync_interval, + 'stats_cache_interval': cluster_settings.stats_cache_interval, + 'cluster_mode': cluster_settings.cluster_mode, + 'restart_mode': cluster_settings.restart_mode, + 'config_version': cluster_settings.config_version, + }, + } + + return JsonResponse(data, status=200) diff --git a/cluster/migrations/0006_worker_error_status.py b/cluster/migrations/0006_worker_error_status.py new file mode 100644 index 0000000..7660227 --- /dev/null +++ b/cluster/migrations/0006_worker_error_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-29 21:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0005_alter_worker_token'), + ] + + operations = [ + migrations.AddField( + model_name='worker', + name='error_status', + field=models.CharField(choices=[('', ''), ('ip_lock', 'IP lock is enabled, but the worker is attempting to access from a different IP address.'), ('worker_disabled', 'Worker is not enabled'), ('cluster_disabled', 'Cluster is not enabled')], default='', max_length=32), + ), + ] diff --git a/cluster/migrations/0007_workerstatus_worker_version.py b/cluster/migrations/0007_workerstatus_worker_version.py new file mode 100644 index 0000000..78b1e4a --- /dev/null +++ b/cluster/migrations/0007_workerstatus_worker_version.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-30 12:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0006_worker_error_status'), + ] + + operations = [ + migrations.AddField( + model_name='workerstatus', + name='worker_version', + field=models.BooleanField(default=0), + ), + ] diff --git a/cluster/migrations/0008_alter_worker_error_status.py b/cluster/migrations/0008_alter_worker_error_status.py new file mode 100644 index 0000000..8c79b89 --- /dev/null +++ b/cluster/migrations/0008_alter_worker_error_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-30 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0007_workerstatus_worker_version'), + ] + + operations = [ + migrations.AlterField( + model_name='worker', + name='error_status', + field=models.CharField(choices=[('', ''), ('ip_lock', 'IP lock is enabled, but the worker is attempting to access from a different IP address.'), ('worker_disabled', 'Worker is not enabled'), ('cluster_disabled', 'Cluster is not enabled'), ('missing_version', 'Please report worker_config_version and worker_version in the API request.')], default='', max_length=32), + ), + ] diff --git a/cluster/migrations/0009_alter_worker_error_status.py b/cluster/migrations/0009_alter_worker_error_status.py new file mode 100644 index 0000000..394fe3f --- /dev/null +++ b/cluster/migrations/0009_alter_worker_error_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-30 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0008_alter_worker_error_status'), + ] + + operations = [ + migrations.AlterField( + model_name='worker', + name='error_status', + field=models.CharField(choices=[('', ''), ('ip_lock', 'IP lock is enabled, but the worker is attempting to access from a different IP address.'), ('worker_disabled', 'Worker is not enabled'), ('cluster_disabled', 'Cluster is not enabled'), ('missing_version', 'Please report worker_config_version and worker_version in the API request.'), ('update_required', 'Worker configuration update is required.')], default='', max_length=32), + ), + ] diff --git a/cluster/migrations/0010_alter_worker_error_status_and_more.py b/cluster/migrations/0010_alter_worker_error_status_and_more.py new file mode 100644 index 0000000..088eb58 --- /dev/null +++ b/cluster/migrations/0010_alter_worker_error_status_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.9 on 2025-12-30 14:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0009_alter_worker_error_status'), + ] + + operations = [ + migrations.AlterField( + model_name='worker', + name='error_status', + field=models.CharField(choices=[('', ''), ('ip_lock', 'IP lock is enabled, but the worker is attempting to access from a different IP address.'), ('worker_disabled', 'Worker is not enabled'), ('cluster_disabled', 'Cluster is not enabled'), ('missing_version', 'Please report worker_config_version and worker_version in the API request.'), ('update_required', 'Worker update is required.')], default='', max_length=32), + ), + migrations.AlterField( + model_name='workerstatus', + name='worker_version', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/cluster/models.py b/cluster/models.py index 01c8f8c..3ca7842 100644 --- a/cluster/models.py +++ b/cluster/models.py @@ -1,6 +1,7 @@ import uuid from django.db import models +from django.utils.translation import gettext_lazy as _ class ClusterSettings(models.Model): @@ -41,6 +42,15 @@ class Worker(models.Model): city = models.CharField(max_length=100, blank=True, null=True) hostname = models.CharField(max_length=100, blank=True, null=True) + error_status = models.CharField(default='', max_length=32, choices=( + ('', ''), + ('ip_lock', _('IP lock is enabled, but the worker is attempting to access from a different IP address.')), + ('worker_disabled', _('Worker is not enabled')), + ('cluster_disabled', _('Cluster is not enabled')), + ('missing_version', _('Please report worker_config_version and worker_version in the API request.')), + ('update_required', _('Worker update is required.')) + )) + updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) @@ -53,6 +63,7 @@ class WorkerStatus(models.Model): last_restart = models.DateTimeField(blank=True, null=True) config_version = models.PositiveIntegerField(default=0) config_pending = models.BooleanField(default=False) + worker_version = models.PositiveIntegerField(default=0) active_peers = models.PositiveIntegerField(default=0) wireguard_status = models.JSONField(default=dict) diff --git a/cluster/views.py b/cluster/views.py index 29ca03e..cb750bf 100644 --- a/cluster/views.py +++ b/cluster/views.py @@ -16,7 +16,7 @@ def cluster_main(request): page_title = _('Cluster') workers = Worker.objects.all().order_by('name') - context = {'page_title': page_title, 'workers': workers} + context = {'page_title': page_title, 'workers': workers, 'worker_version': 10} return render(request, 'cluster/workers_list.html', context) diff --git a/templates/cluster/workers_list.html b/templates/cluster/workers_list.html index 4023c0c..3a98deb 100644 --- a/templates/cluster/workers_list.html +++ b/templates/cluster/workers_list.html @@ -20,10 +20,17 @@ {{ worker.name }} - {% if worker.enabled %} - + {% if worker.error_status %} + {% else %} - + {% if worker.enabled %} + + {% else %} + + {% endif %} + {% endif %} + {% if worker_version > worker.workerstatus.worker_version %} + {% endif %} diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index b51f9c2..1ad3895 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -14,12 +14,14 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.contrib import admin from django.urls import path from accounts.views import view_create_first_user, view_login, view_logout from api.views import api_instance_info, api_peer_invite, api_peer_list, cron_check_updates, \ cron_update_peer_latest_handshake, peer_info, routerfleet_authenticate_session, routerfleet_get_user_token, \ wireguard_status +from cluster.cluster_api import api_cluster_status, api_get_worker_config_files from cluster.views import cluster_main, cluster_settings, worker_manage from console.views import view_console from dns.views import view_apply_dns_config, view_manage_dns_settings, view_manage_filter_list, view_manage_static_host, \ @@ -38,7 +40,7 @@ from wireguard_peer.views import view_manage_ip_address, view_wireguard_peer_lis from wireguard_tools.views import download_config_or_qrcode, export_wireguard_configs, restart_wireguard_interfaces urlpatterns = [ - # path('admin/', admin.site.urls), + path('admin/', admin.site.urls), path('', view_apply_db_patches, name='apply_db_patches'), path('status/', view_wireguard_status, name='wireguard_status'), path('dns/', view_static_host_list, name='static_host_list'), @@ -74,6 +76,8 @@ urlpatterns = [ path('api/peer_invite/', api_peer_invite, name='api_peer_invite'), path('api/cron_check_updates/', cron_check_updates, name='cron_check_updates'), path('api/cron_update_peer_latest_handshake/', cron_update_peer_latest_handshake, name='cron_update_peer_latest_handshake'), + path('api/cluster/status/', api_cluster_status, name='api_cluster_status'), + path('api/cluster/worker/get_config_files/', api_get_worker_config_files, name='api_get_worker_config_files'), path('firewall/port_forward/', view_redirect_rule_list, name='redirect_rule_list'), path('firewall/manage_port_forward_rule/', manage_redirect_rule, name='manage_redirect_rule'), path('firewall/rule_list/', view_firewall_rule_list, name='firewall_rule_list'),