Browse Source

refactor(api): put pdns communication in pdns.py

Nils Wisiol 8 years ago
parent
commit
6ecb3a3ac3

+ 46 - 85
api/desecapi/models.py

@@ -4,10 +4,7 @@ from django.contrib.auth.models import (
     BaseUserManager, AbstractBaseUser
 )
 from django.utils import timezone
-import requests
-import json
-import subprocess
-import os
+from desecapi import pdns
 import datetime, time
 
 
@@ -85,6 +82,12 @@ class User(AbstractBaseUser):
         # Simplest possible answer: All admins are staff
         return self.is_admin
 
+    def unlock(self):
+        self.captcha_required = False
+        for domain in self.domain_set:
+            domain.pdns_sync()
+        self.save()
+
 
 class Domain(models.Model):
     created = models.DateTimeField(auto_now_add=True)
@@ -95,90 +98,48 @@ class Domain(models.Model):
     dyn = models.BooleanField(default=False)
     owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
 
-    headers = {
-        'User-Agent': 'desecapi',
-        'X-API-Key': settings.POWERDNS_API_TOKEN,
-    }
+    def pdns_resync(self):
+        """
+        Make sure that pdns gets the latest information about this domain/zone.
+        Re-Syncing is relatively expensive and should not happen routinely.
+        """
+
+        # Create zone if needed
+        if not pdns.zone_exists(self.name):
+            pdns.create_native_zone(self.name)
+
+        # update zone to latest information
+        pdns.set_dyn_records(self.name, self.arecord, self.aaaarecord)
+
+    def pdns_sync(self):
+        """
+        Command pdns updates as indicated by the local changes.
+        """
+
+        if self.owner.captcha_required:
+            # suspend all updates
+            pass
+
+        new_domain = self.id is None
+        changes_required = False
+
+        # if this zone is new, create it
+        if new_domain:
+            pdns.create_native_zone(self.name)
+
+        # for existing domains, see if records are changed
+        if not new_domain:
+            orig_domain = Domain.objects.get(id=self.id)
+            changes_required = self.arecord != orig_domain.arecord or self.aaaarecord != orig_domain.aaaarecord
+
+        # make changes if necessary
+        if changes_required:
+            pdns.set_dyn_records(self.name, self.arecord, self.aaaarecord)
 
     def save(self, *args, **kwargs):
-        if self.id is None:
-            self.pdnsCreate()
-            if self.arecord or self.aaaarecord:
-                self.pdnsUpdate()
-        else:
-            orig = Domain.objects.get(id=self.id)
-            if self.arecord != orig.arecord or self.aaaarecord != orig.aaaarecord:
-                self.pdnsUpdate()
         self.updated = timezone.now()
-        super(Domain, self).save(*args, **kwargs) # Call the "real" save() method.
-
-    def pdnsCreate(self):
-        payload = {
-            "name": self.name + ".",
-            "kind": "NATIVE",
-            "masters": [],
-            "nameservers": [
-                "ns1.desec.io.",
-                "ns2.desec.io."
-            ]
-        }
-        r = requests.post(settings.POWERDNS_API + '/zones', data=json.dumps(payload), headers=self.headers)
-        if r.status_code < 200 or r.status_code >= 300:
-            raise Exception(r.text)
-
-    def pdnsUpdate(self):
-        if self.arecord:
-            a = \
-                {
-                    "records": [
-                            {
-                                "type": "A",
-                                "name": self.name + ".",
-                                "disabled": False,
-                                "content": self.arecord,
-                            }
-                        ],
-                    "ttl": 60,
-                    "changetype": "REPLACE",
-                    "type": "A",
-                    "name": self.name + ".",
-                }
-        else:
-            a = \
-                {
-                    "changetype": "DELETE",
-                    "type": "A",
-                    "name": self.name + "."
-                }
-
-        if self.aaaarecord:
-            aaaa = \
-                {
-                    "records": [
-                            {
-                                "type": "AAAA",
-                                "name": self.name + ".",
-                                "disabled": False,
-                                "content": self.aaaarecord,
-                            }
-                        ],
-                    "ttl": 60,
-                    "changetype": "REPLACE",
-                    "type": "AAAA",
-                    "name": self.name + ".",
-                }
-        else:
-            aaaa = \
-                {
-                    "changetype": "DELETE",
-                    "type": "AAAA",
-                    "name": self.name + "."
-                }
-
-        payload = { "rrsets": [a, aaaa] }
-        r = requests.patch(settings.POWERDNS_API + '/zones/' + self.name, data=json.dumps(payload), headers=self.headers)
-        if r.status_code < 200 or r.status_code >= 300:
-            raise Exception(r)
+        self.pdns_sync()
+        super(Domain, self).save(*args, **kwargs)
 
     class Meta:
         ordering = ('created',)

+ 104 - 0
api/desecapi/pdns.py

@@ -0,0 +1,104 @@
+import requests
+import json
+from desecapi import settings
+
+
+headers = {
+    'User-Agent': 'desecapi',
+    'X-API-Key': settings.POWERDNS_API_TOKEN,
+}
+
+
+def normalize_hostname(name):
+    if '/' in name or '?' in name:
+        raise Exception('Invalid hostname ' + name)
+    return name if name.endswith('.') else name + '.'
+
+
+def _pdns_post(url, body):
+    r = requests.post(settings.POWERDNS_API + url, data=json.dumps(body), headers=headers)
+    if r.status_code < 200 or r.status_code >= 300:
+        raise Exception(r.text)
+    return r
+
+
+def _pdns_patch(url, body):
+    r = requests.patch(settings.POWERDNS_API + url, data=json.dumps(body), headers=headers)
+    if r.status_code < 200 or r.status_code >= 300:
+        raise Exception(r.text)
+    return r
+
+
+def _pdns_get(url):
+    r = requests.get(settings.POWERDNS_API + url, headers=headers)
+    if (r.status_code < 200 or r.status_code >= 300) and r.status_code != 404:
+        raise Exception(r.text)
+    return r
+
+
+def _delete_or_replace(name, type, value):
+    """
+    Return pdns API json to either replace or delete a record, depending on value is empty or not.
+    """
+    if value != "":
+        return \
+            {
+                "records": [
+                    {
+                        "type": type,
+                        "name": name,
+                        "disabled": False,
+                        "content": value,
+                    }
+                ],
+                "ttl": 60,
+                "changetype": "REPLACE",
+                "type": type,
+                "name": name,
+            }
+    else:
+        return \
+            {
+                "changetype": "DELETE",
+                "type": type,
+                "name": name
+            }
+
+
+def create_native_zone(name):
+    """
+    Commands pdns to create a zone with the given name.
+    """
+    payload = {
+        "name": normalize_hostname(name),
+        "kind": "NATIVE",
+        "masters": [],
+        "nameservers": [
+            "ns1.desec.io.",
+            "ns2.desec.io."
+        ]
+    }
+    _pdns_post('/zones', payload)
+
+
+def get_zone_exists(name):
+    """
+    Returns whether pdns knows a zone with the given name.
+    """
+    return _pdns_get('/zones/' + normalize_hostname(name)).status_code != 404
+
+
+def set_dyn_records(name, a, aaaa):
+    """
+    Commands pdns to set the A and AAAA record for the zone with the given name to the given record values.
+    Only supports one A, one AAAA record.
+    If a or aaaa is None, pdns will be commanded to delete the record.
+    """
+    name = normalize_hostname(name)
+
+    _pdns_patch('/zones/' + name, {
+        "rrsets": [
+            _delete_or_replace(name, 'a', a),
+            _delete_or_replace(name, 'aaaa', aaaa),
+        ]
+    })

+ 1 - 1
api/desecapi/tests/testdomains.py

@@ -150,7 +150,7 @@ class AuthenticatedDomainTests(APITestCase):
         response = self.client.get(url)
 
         httpretty.enable()
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + response.data['name'])
+        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + response.data['name'] + '.')
 
         response.data['arecord'] = '10.13.3.7'
         response = self.client.put(url, response.data)

+ 1 - 1
api/desecapi/tests/testdyndns12update.py

@@ -32,7 +32,7 @@ class DynDNS12UpdateTest(APITestCase):
 
         httpretty.enable()
         httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain)
+        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain + '.')
 
     def assertIP(self, ipv4=None, ipv6=None):
         old_credentials = self.client._credentials['HTTP_AUTHORIZATION']

+ 1 - 1
api/desecapi/tests/testdynupdateauthentication.py

@@ -32,7 +32,7 @@ class DynUpdateAuthenticationTests(APITestCase):
 
             httpretty.enable()
             httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-            httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain)
+            httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain + '.')
 
     def testSuccessfulAuthentication(self):
         response = self.client.get(self.url)

+ 1 - 3
api/desecapi/views.py

@@ -308,9 +308,7 @@ def unlock(request, email):
         # check whether it's valid:
         if form.is_valid():
             try:
-                user = User.objects.get(email=email)
-                user.captcha_required = False
-                user.save()
+                User.objects.get(email=email).unlock()
             except User.DoesNotExist:
                 pass # fail silently, otherwise people can find out if email addresses are registered with us