From ee3d416659d02929873f3f12f6da391d0f612f77 Mon Sep 17 00:00:00 2001 From: Rens Houben Date: Mon, 20 Oct 2025 11:22:13 +0200 Subject: [PATCH] Initial start of the context/server model, including test framework. --- .gitignore | 2 +- src/config/settings.py | 6 +- src/pdns/__init__.py | 2 + src/pdns/migrations/0001_initial.py | 65 ++++++++++++ ..._alive_pdnsserver_last_checked_and_more.py | 34 +++++++ ..._fk_network_pdnsserver_context_and_more.py | 29 ++++++ src/pdns/migrations/__init__.py | 0 src/pdns/models.py | 99 +++++++++++++++++++ src/pdns/test_models.py | 73 ++++++++++++++ src/tests/basedata.py | 0 10 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/pdns/__init__.py create mode 100644 src/pdns/migrations/0001_initial.py create mode 100644 src/pdns/migrations/0002_pdnsserver_alive_pdnsserver_last_checked_and_more.py create mode 100644 src/pdns/migrations/0003_rename_fk_network_pdnsserver_context_and_more.py create mode 100644 src/pdns/migrations/__init__.py create mode 100644 src/pdns/models.py create mode 100644 src/pdns/test_models.py create mode 100644 src/tests/basedata.py diff --git a/.gitignore b/.gitignore index 1d27038..297f594 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ *.pot *.pyc __pycache__ -db.sqlite3 +*.sqlite3 media staticfiles/ diff --git a/src/config/settings.py b/src/config/settings.py index 519a5ce..5ef4401 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -32,6 +32,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "allauth", "allauth.account", + 'pdns', ] AUTHENTICATION_BACKENDS = [ @@ -50,6 +51,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.locale.LocaleMiddleware", "allauth.account.middleware.AccountMiddleware", ] @@ -77,7 +79,7 @@ DEFAULT_DB = { 'ENGINE': f"django.db.backends.{DB_ENGINE}", } if DB_ENGINE=='sqlite3': - DEFAULT_DB['NAME'] = os.path.join('BASE_DIR', + DEFAULT_DB['NAME'] = os.path.join(BASE_DIR, env('DB_NAME', default='pdanext.sqlite3')) else: DEFAULT_DB['NAME'] = env('DB_NAME') @@ -93,6 +95,8 @@ DATABASES = { 'default': DEFAULT_DB, } +print('Database: %s' % DATABASES['default']) if DEBUG else None + AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", diff --git a/src/pdns/__init__.py b/src/pdns/__init__.py new file mode 100644 index 0000000..a25e00f --- /dev/null +++ b/src/pdns/__init__.py @@ -0,0 +1,2 @@ +# __init__.py needs to exist or django won't load the module. + diff --git a/src/pdns/migrations/0001_initial.py b/src/pdns/migrations/0001_initial.py new file mode 100644 index 0000000..d57505f --- /dev/null +++ b/src/pdns/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.5 on 2025-10-20 04:54 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="PDNSContext", + fields=[ + ( + "name", + models.CharField( + max_length=20, + primary_key=True, + serialize=False, + unique=True, + verbose_name="Name", + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ], + ), + migrations.CreateModel( + name="PDNSServer", + fields=[ + ( + "name", + models.CharField( + max_length=40, + primary_key=True, + serialize=False, + unique=True, + verbose_name="Name", + ), + ), + ( + "server_uri", + models.URLField( + help_text="Prefer IP addresses; use FQDN at your own risk.", + max_length=80, + verbose_name="Server URL", + ), + ), + ( + "fk_network", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="servers", + to="pdns.pdnscontext", + verbose_name="Context", + ), + ), + ], + ), + ] diff --git a/src/pdns/migrations/0002_pdnsserver_alive_pdnsserver_last_checked_and_more.py b/src/pdns/migrations/0002_pdnsserver_alive_pdnsserver_last_checked_and_more.py new file mode 100644 index 0000000..0e544c9 --- /dev/null +++ b/src/pdns/migrations/0002_pdnsserver_alive_pdnsserver_last_checked_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.5 on 2025-10-20 07:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pdns", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="pdnsserver", + name="alive", + field=models.BooleanField( + default=False, editable=False, verbose_name="Alive status" + ), + ), + migrations.AddField( + model_name="pdnsserver", + name="last_checked", + field=models.DateTimeField( + editable=False, null=True, verbose_name="Last checked" + ), + ), + migrations.AddField( + model_name="pdnsserver", + name="last_seen", + field=models.DateTimeField( + editable=False, null=True, verbose_name="Last seen online" + ), + ), + ] diff --git a/src/pdns/migrations/0003_rename_fk_network_pdnsserver_context_and_more.py b/src/pdns/migrations/0003_rename_fk_network_pdnsserver_context_and_more.py new file mode 100644 index 0000000..b32fe3e --- /dev/null +++ b/src/pdns/migrations/0003_rename_fk_network_pdnsserver_context_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.5 on 2025-10-20 08:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pdns", "0002_pdnsserver_alive_pdnsserver_last_checked_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="pdnsserver", + old_name="fk_network", + new_name="context", + ), + migrations.RemoveField( + model_name="pdnsserver", + name="alive", + ), + migrations.AddField( + model_name="pdnsserver", + name="online", + field=models.BooleanField( + default=False, editable=False, verbose_name="Online status" + ), + ), + ] diff --git a/src/pdns/migrations/__init__.py b/src/pdns/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pdns/models.py b/src/pdns/models.py new file mode 100644 index 0000000..b7ac0c6 --- /dev/null +++ b/src/pdns/models.py @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +# +# + +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext as _ + +# Okay, going to illustrate my thinking here: +# A ##Context## is a set of servers that operate together as +# authoritative for a group of zones. + +# (I was going to call it a Network, but then I found out +# about https://doc.powerdns.com/authoritative/views.html ) + +# All ##Servers## within a Context are presumed to be +# using the same database and be otherwise interchangeable, +# Although given the limitations of some backends they can +# be set to read-only. + +class PDNSContext(models.Model): + name = models.CharField(verbose_name=_('Name'), + max_length=20, + unique=True, + primary_key=True, + ) + + description = models.TextField(verbose_name=_('Description'), + blank=True, + null=True, + ) + + def __str__(self): + return self.name + + def total_servers(self): + return self.servers.all().count() + + def online_servers(self): + return self.servers.filter(online=True).count() + + def offline_servers(self): + return self.servers.filter(online=False).count() + + def status(self): + return _('Context %(name)s: %(total)d total, %(online)d online, %(offline)d offline.') % { + 'name': self.name, + 'total': self.total_servers(), + 'online': self.online_servers(), + 'offline': self.offline_servers()} + + +class PDNSServer(models.Model): + name = models.CharField(verbose_name=_('Name'), + max_length=40, + unique=True, + primary_key=True, + ) + + server_uri = models.URLField(verbose_name=_('Server URL'), + help_text='Prefer IP addresses; use FQDN at your own risk.', + max_length=80, + blank=False, + null=False + ) + + context=models.ForeignKey(PDNSContext, + verbose_name=_('Context'), + on_delete=models.PROTECT, + related_name='servers' + ) + + online = models.BooleanField(verbose_name=_('Online status'), + editable=False, + default=False + ) + + last_checked = models.DateTimeField(verbose_name=_('Last checked'), + editable=False, + null=True + ) + + last_seen = models.DateTimeField(verbose_name=_('Last seen online'), + editable=False, + null=True + ) + + def __str__(self): + selfstr= _("%(name)s (%(context)s): %(url)s") % { 'name': self.name, + 'context': self.context.name, + 'url': self.server_uri} + return selfstr + + def status(self): + status = _('Online') if self.online else _('Offline') + return _('%(status)s since %(time)s') % { + 'status': status, + 'time': self.last_checked + } diff --git a/src/pdns/test_models.py b/src/pdns/test_models.py new file mode 100644 index 0000000..fe40be6 --- /dev/null +++ b/src/pdns/test_models.py @@ -0,0 +1,73 @@ +#!/usr/bin/python3 + +from django.test import TestCase +from django.utils.timezone import now +from django.utils.translation import gettext as _ +from unittest import skip + +from .models import ( + PDNSContext, + PDNSServer +) + +# Tests are run in transactions, which means that for every test_*() function +# the database restores to what's created during setUp() + +class PDNSModelTestCase(TestCase): + + def setUp(self): + self.test_time = now() + tc = PDNSContext.objects.create(name='test context') + PDNSServer.objects.create(name='test server', + server_uri='https://localhost:8081', + context=tc, + online=True, + last_seen=self.test_time, + last_checked=self.test_time) + self.context_target = 'test context' + self.server_target = 'test server (test context): https://localhost:8081' + self.server_status_target = _('%(status)s since %(date)s') % { + 'status': _('Online'), + 'date': self.test_time + } + self.server_status_offline_target = _('%(status)s since %(date)s') % { + 'status': _('Offline'), + 'date': self.test_time + } + return True + + def test_server(self): + test_server = PDNSServer.objects.get(name='test server') + self.assertEqual(test_server.__str__(), self.server_target) + self.assertEqual(test_server.status(), self.server_status_target) + + def test_server_offline(self): + test_server = PDNSServer.objects.get(name='test server') + test_server.online = False + self.assertEqual(test_server.status(), self.server_status_offline_target) + + def test_context(self): + test_context = PDNSContext.objects.get(name='test context') + self.assertEqual(test_context.__str__(), self.context_target) + + def test_context_onoff(self): + test_context = PDNSContext.objects.get(name='test context') + self.assertEqual(test_context.total_servers(),1) + self.assertEqual(test_context.online_servers(),1) + self.assertEqual(test_context.offline_servers(),0) + + def test_context_offline(self): + test_context = PDNSContext.objects.get(name='test context') + test_server = PDNSServer.objects.get(name='test server') + test_server.online = False + test_server.save() + self.assertEqual(test_context.total_servers(),1) + self.assertEqual(test_context.online_servers(),0) + self.assertEqual(test_context.offline_servers(),1) + + + + + + + diff --git a/src/tests/basedata.py b/src/tests/basedata.py new file mode 100644 index 0000000..e69de29