From d3033d39369fcd651ad487a45786cba301ec1b71 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 28 Jan 2026 11:57:54 -0300 Subject: [PATCH] Add scheduling models and fields for peer management --- scheduler/__init__.py | 0 scheduler/admin.py | 1 + scheduler/apps.py | 6 ++ scheduler/migrations/0001_initial.py | 66 ++++++++++++++++++ scheduler/migrations/__init__.py | 0 scheduler/models.py | 69 +++++++++++++++++++ scheduler/tests.py | 1 + scheduler/views.py | 1 + ...y_schedule_peer_suspend_reason_and_more.py | 28 ++++++++ wireguard/models.py | 8 +++ wireguard_webadmin/settings.py | 3 +- 11 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 scheduler/__init__.py create mode 100644 scheduler/admin.py create mode 100644 scheduler/apps.py create mode 100644 scheduler/migrations/0001_initial.py create mode 100644 scheduler/migrations/__init__.py create mode 100644 scheduler/models.py create mode 100644 scheduler/tests.py create mode 100644 scheduler/views.py create mode 100644 wireguard/migrations/0030_peer_enabled_by_schedule_peer_suspend_reason_and_more.py diff --git a/scheduler/__init__.py b/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scheduler/admin.py b/scheduler/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/scheduler/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/scheduler/apps.py b/scheduler/apps.py new file mode 100644 index 0000000..3a3846a --- /dev/null +++ b/scheduler/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SchedulerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'scheduler' diff --git a/scheduler/migrations/0001_initial.py b/scheduler/migrations/0001_initial.py new file mode 100644 index 0000000..67e8412 --- /dev/null +++ b/scheduler/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 5.2.9 on 2026-01-28 14:57 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wireguard', '0030_peer_enabled_by_schedule_peer_suspend_reason_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ScheduleProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), + ], + ), + migrations.CreateModel( + name='PeerScheduling', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('next_scheduled_enable_at', models.DateTimeField(blank=True, null=True)), + ('next_scheduled_disable_at', models.DateTimeField(blank=True, null=True)), + ('schedule_last_calculated_at', models.DateTimeField(blank=True, null=True)), + ('next_manual_suspend_at', models.DateTimeField(blank=True, null=True)), + ('next_manual_unsuspend_at', models.DateTimeField(blank=True, null=True)), + ('manual_suspend_reason', models.TextField(blank=True, null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), + ('peer', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='schedule', to='wireguard.peer')), + ('profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='scheduler.scheduleprofile')), + ], + options={ + 'indexes': [models.Index(fields=['next_scheduled_enable_at'], name='scheduler_p_next_sc_ef2e87_idx'), models.Index(fields=['next_scheduled_disable_at'], name='scheduler_p_next_sc_203dba_idx'), models.Index(fields=['next_manual_suspend_at'], name='scheduler_p_next_ma_31b89c_idx'), models.Index(fields=['next_manual_unsuspend_at'], name='scheduler_p_next_ma_b980b8_idx')], + }, + ), + migrations.CreateModel( + name='ScheduleSlot', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_weekday', models.PositiveSmallIntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])), + ('end_weekday', models.PositiveSmallIntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])), + ('start_time', models.TimeField()), + ('end_time', models.TimeField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='scheduler.scheduleprofile')), + ], + options={ + 'ordering': ('start_weekday', 'start_time'), + 'constraints': [models.UniqueConstraint(fields=('profile', 'start_weekday', 'end_weekday', 'start_time', 'end_time'), name='uniq_slot_per_profile')], + }, + ), + ] diff --git a/scheduler/migrations/__init__.py b/scheduler/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scheduler/models.py b/scheduler/models.py new file mode 100644 index 0000000..f99638b --- /dev/null +++ b/scheduler/models.py @@ -0,0 +1,69 @@ +import uuid + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from wireguard.models import Peer + +WEEK_DAYS = [ + (0, _("Monday")), + (1, _("Tuesday")), + (2, _("Wednesday")), + (3, _("Thursday")), + (4, _("Friday")), + (5, _("Saturday")), + (6, _("Sunday")), +] + +class ScheduleProfile(models.Model): + name = models.CharField(max_length=100) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(editable=False, default=uuid.uuid4) + + +class ScheduleSlot(models.Model): + profile = models.ForeignKey(ScheduleProfile, on_delete=models.CASCADE, related_name="slots") + start_weekday = models.PositiveSmallIntegerField(choices=WEEK_DAYS) + end_weekday = models.PositiveSmallIntegerField(choices=WEEK_DAYS) + start_time = models.TimeField() + end_time = models.TimeField() + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(editable=False, default=uuid.uuid4) + + class Meta: + ordering = ("start_weekday", "start_time") + constraints = [ + models.UniqueConstraint( + fields=["profile", "start_weekday", "end_weekday", "start_time", "end_time"], + name="uniq_slot_per_profile" + ), + ] + + +class PeerScheduling(models.Model): + peer = models.OneToOneField(Peer, on_delete=models.CASCADE, related_name="schedule") + profile = models.ForeignKey(ScheduleProfile, on_delete=models.SET_NULL, null=True, blank=True) + + next_scheduled_enable_at = models.DateTimeField(null=True, blank=True) + next_scheduled_disable_at = models.DateTimeField(null=True, blank=True) + schedule_last_calculated_at = models.DateTimeField(null=True, blank=True) + + next_manual_suspend_at = models.DateTimeField(null=True, blank=True) + next_manual_unsuspend_at = models.DateTimeField(null=True, blank=True) + manual_suspend_reason = models.TextField(null=True, blank=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(editable=False, default=uuid.uuid4) + + class Meta: + indexes = [ + models.Index(fields=["next_scheduled_enable_at"]), + models.Index(fields=["next_scheduled_disable_at"]), + models.Index(fields=["next_manual_suspend_at"]), + models.Index(fields=["next_manual_unsuspend_at"]), + ] \ No newline at end of file diff --git a/scheduler/tests.py b/scheduler/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/scheduler/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/scheduler/views.py b/scheduler/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/scheduler/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/wireguard/migrations/0030_peer_enabled_by_schedule_peer_suspend_reason_and_more.py b/wireguard/migrations/0030_peer_enabled_by_schedule_peer_suspend_reason_and_more.py new file mode 100644 index 0000000..d3c8198 --- /dev/null +++ b/wireguard/migrations/0030_peer_enabled_by_schedule_peer_suspend_reason_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.9 on 2026-01-28 14:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireguard', '0029_wireguardinstance_enforce_route_policy'), + ] + + operations = [ + migrations.AddField( + model_name='peer', + name='enabled_by_schedule', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='peer', + name='suspend_reason', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='peer', + name='suspended', + field=models.BooleanField(default=False), + ), + ] diff --git a/wireguard/models.py b/wireguard/models.py index e46ff29..193bd47 100644 --- a/wireguard/models.py +++ b/wireguard/models.py @@ -119,6 +119,10 @@ class Peer(models.Model): 'routing_templates.RoutingTemplate', on_delete=models.SET_NULL, blank=True, null=True, related_name='peers' ) + enabled_by_schedule = models.BooleanField(default=True) + suspended = models.BooleanField(default=False) + suspend_reason = models.TextField(blank=True, null=True) + created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4) @@ -129,6 +133,10 @@ class Peer(models.Model): else: return self.public_key[:16] + "..." + @property + def enabled(self) -> bool: + return self.enabled_by_schedule and not self.suspended + @property def announced_networks(self): prefetched = getattr(self, "_prefetched_objects_cache", {}) diff --git a/wireguard_webadmin/settings.py b/wireguard_webadmin/settings.py index 74f4250..07c8f29 100644 --- a/wireguard_webadmin/settings.py +++ b/wireguard_webadmin/settings.py @@ -48,7 +48,8 @@ INSTALLED_APPS = [ 'vpn_invite', 'cluster', 'api', - 'routing_templates' + 'routing_templates', + 'scheduler' ] MIDDLEWARE = [