Kaynağa Gözat

feat(api): implement zone deletion

We're also removing DomainDetail.put(). Rationale:

- It contained a permission check that was already guaranteed by permission_classes (isOwner)
- It called the put() method from the parent class
- In the current put() implementation, requests via DomainDetailByName are broken

For id-based requests, the behavior with DomainDetail.put() removed is equivalent,
and removing the implementation fixes name-based requests.
Peter Thomassen 8 yıl önce
ebeveyn
işleme
31ee6d32cd

+ 1 - 0
.env.default

@@ -33,6 +33,7 @@ DESECSTACK_DBMASTER_PASSWORD_ns1replication=
 DESECSTACK_DBMASTER_SUBJECT_ns1replication=ns1.desec.io
 DESECSTACK_DBMASTER_PASSWORD_ns2replication=
 DESECSTACK_DBMASTER_SUBJECT_ns2replication=ns2.desec.io
+DESECSTACK_NSMASTER_APIKEY=
 DESECSTACK_NSMASTER_CARBONSERVER=
 DESECSTACK_NSMASTER_CARBONOURNAME=
 

+ 1 - 0
.travis.yml

@@ -22,6 +22,7 @@ env:
    - DESECSTACK_DB_SUBJECT_ns2replication=9Fn33T5yGukjnrtj
    - DESECSTACK_DEVADMIN_PASSWORDmd5=.
    - DESECSTACK_NSLORD_APIKEY=9Fn33T5yGukjekwjew
+   - DESECSTACK_NSMASTER_APIKEY=LLq1orOQuXCINUz4TV
    - DESECSTACK_IPV4_REAR_PREFIX16=172.19
    - DESECSTACK_IPV6_SUBNET=fd80::/8
    - DESECSTACK_IPV6_ADDRESS=fd80::1

+ 1 - 0
README.md

@@ -54,6 +54,7 @@ Although most configuration is contained in this repository, some external depen
       - `DESECSTACK_DBMASTER_SUBJECT_ns1replication`: slave 1 replication SSL certificate subject name
       - `DESECSTACK_DBMASTER_PASSWORD_ns2replication`: slave 2 replication password
       - `DESECSTACK_DBMASTER_SUBJECT_ns2replication`: slave 1 replication SSL certificate subject name
+      - `DESECSTACK_NSMASTER_APIKEY`: pdns API key on nsmaster (required so that we can execute zone deletions on nsmaster, which replicates to the slaves)
       - `DESECSTACK_NSMASTER_CARBONSERVER`: pdns `carbon-server` setting on nsmaster (optional)
       - `DESECSTACK_NSMASTER_CARBONOURNAME`: pdns `carbon-ourname` setting on nsmaster (optional)
     - devadmin-related

+ 7 - 0
api/desecapi/models.py

@@ -139,6 +139,13 @@ class Domain(models.Model):
         if changes_required:
             pdns.set_dyn_records(self.name, self.arecord, self.aaaarecord)
 
+    def delete(self, *args, **kwargs):
+        pdns.delete_zone(self.name)
+        if self.name.endswith('.dedyn.io'):
+            pdns.set_rrset('dedyn.io', self.name, 'DS', '')
+            pdns.set_rrset('dedyn.io', self.name, 'NS', '')
+        super(Domain, self).delete(*args, **kwargs)
+
     def save(self, *args, **kwargs):
         self.updated = timezone.now()
         self.pdns_sync()

+ 52 - 8
api/desecapi/pdns.py

@@ -3,9 +3,14 @@ import json
 from desecapi import settings
 
 
-headers = {
+headers_nslord = {
     'User-Agent': 'desecapi',
-    'X-API-Key': settings.POWERDNS_API_TOKEN,
+    'X-API-Key': settings.NSLORD_PDNS_API_TOKEN,
+}
+
+headers_nsmaster = {
+    'User-Agent': 'desecapi',
+    'X-API-Key': settings.NSMASTER_PDNS_API_TOKEN,
 }
 
 
@@ -14,23 +19,40 @@ def normalize_hostname(name):
         raise Exception('Invalid hostname ' + name)
     return name if name.endswith('.') else name + '.'
 
+def _pdns_delete(url):
+    # We first delete the zone from nslord, the main authoritative source of our DNS data.
+    # However, we do not want to wait for the zone to expire on the slave ("nsmaster").
+    # We thus issue a second delete request on nsmaster to delete the zone there immediately.
+    r1 = requests.delete(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
+    if r1.status_code < 200 or r1.status_code >= 300:
+        raise Exception(r1.text)
+
+    # Delete from nsmaster as well
+    r2 = requests.delete(settings.NSMASTER_PDNS_API + url, headers=headers_nsmaster)
+    if r2.status_code < 200 or r2.status_code >= 300:
+        # Allow this to fail if nsmaster does not know the zone yet
+        if r2.status_code == 422 and 'Could not find domain' in r2.text:
+            pass
+        else:
+            raise Exception(r2.text)
+
+    return (r1, r2)
 
 def _pdns_post(url, body):
-    r = requests.post(settings.POWERDNS_API + url, data=json.dumps(body), headers=headers)
+    r = requests.post(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
     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)
+    r = requests.patch(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
     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)
+    r = requests.get(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
     if (r.status_code < 200 or r.status_code >= 500):
         raise Exception(r.text)
     return r
@@ -38,7 +60,7 @@ def _pdns_get(url):
 
 def _delete_or_replace_rrset(name, type, value, ttl=60):
     """
-    Return pdns API json to either replace or delete a record set, depending on value is empty or not.
+    Return pdns API json to either replace or delete a record set, depending on whether value is empty or not.
     """
     if value != "":
         return \
@@ -81,6 +103,13 @@ def create_zone(name, kind='NATIVE'):
     _pdns_post('/zones', payload)
 
 
+def delete_zone(name):
+    """
+    Commands pdns to delete a zone with the given name.
+    """
+    _pdns_delete('/zones/' + normalize_hostname(name))
+
+
 def zone_exists(name):
     """
     Returns whether pdns knows a zone with the given name.
@@ -98,7 +127,7 @@ 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.
+    If a or aaaa is empty, pdns will be commanded to delete the record.
     """
     name = normalize_hostname(name)
 
@@ -108,3 +137,18 @@ def set_dyn_records(name, a, aaaa):
             _delete_or_replace_rrset(name, 'aaaa', aaaa),
         ]
     })
+
+
+def set_rrset(zone, name, type, value):
+    """
+    Commands pdns to set or delete a record set for the zone with the given name.
+    If value is empty, the rrset will be deleted.
+    """
+    zone = normalize_hostname(zone)
+    name = normalize_hostname(name)
+
+    _pdns_patch('/zones/' + zone, {
+        "rrsets": [
+            _delete_or_replace_rrset(name, type, value),
+        ]
+    })

+ 4 - 2
api/desecapi/settings.py

@@ -141,8 +141,10 @@ ADMINS = [(address.split("@")[0], address) for address in os.environ['DESECSTACK
 AUTH_USER_MODEL = 'desecapi.User'
 
 # PowerDNS API access
-POWERDNS_API = 'http://nslord:8081/api/v1/servers/localhost'
-POWERDNS_API_TOKEN = os.environ['DESECSTACK_NSLORD_APIKEY']
+NSLORD_PDNS_API = 'http://nslord:8081/api/v1/servers/localhost'
+NSLORD_PDNS_API_TOKEN = os.environ['DESECSTACK_NSLORD_APIKEY']
+NSMASTER_PDNS_API = 'http://nsmaster:8081/api/v1/servers/localhost'
+NSMASTER_PDNS_API_TOKEN = os.environ['DESECSTACK_NSMASTER_APIKEY']
 
 # SEPA direct debit settings
 SEPA = {

+ 60 - 6
api/desecapi/tests/testdomains.py

@@ -33,6 +33,8 @@ class UnauthenticatedDomainTests(APITestCase):
 
 class AuthenticatedDomainTests(APITestCase):
     def setUp(self):
+        httpretty.reset()
+        httpretty.disable()
         if not hasattr(self, 'owner'):
             self.owner = utils.createUser()
             self.ownedDomains = [utils.createDomain(self.owner), utils.createDomain(self.owner)]
@@ -49,16 +51,33 @@ class AuthenticatedDomainTests(APITestCase):
         self.assertEqual(response.data[1]['name'], self.ownedDomains[1].name)
 
     def testCanDeleteOwnedDomain(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.ownedDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.ownedDomains[1].name+ '.')
+
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
         response = self.client.delete(url)
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.assertEqual(httpretty.last_request().method, 'DELETE')
+        self.assertEqual(httpretty.last_request().headers['Host'], 'nsmaster:8081')
+
+        httpretty.reset()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.ownedDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.ownedDomains[1].name+ '.')
+
         response = self.client.get(url)
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertTrue(isinstance(httpretty.last_request(), httpretty.core.HTTPrettyRequestEmpty))
 
     def testCantDeleteOtherDomains(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.otherDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.otherDomains[1].name+ '.')
+
         url = reverse('domain-detail', args=(self.otherDomains[1].pk,))
         response = self.client.delete(url)
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertTrue(isinstance(httpretty.last_request(), httpretty.core.HTTPrettyRequestEmpty))
 
     def testCanGetOwnedDomains(self):
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
@@ -130,7 +149,7 @@ class AuthenticatedDomainTests(APITestCase):
 
     def testPostingCausesPdnsAPICall(self):
         httpretty.enable()
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
 
         url = reverse('domain-list')
         data = {'name': utils.generateDomainname()}
@@ -143,8 +162,8 @@ class AuthenticatedDomainTests(APITestCase):
         name = utils.generateDomainname()
 
         httpretty.enable()
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + name + '.')
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + name + '.')
 
         url = reverse('domain-list')
         data = {'name': name, 'arecord': '1.3.3.7', 'aaaarecord': 'dead::beef'}
@@ -160,7 +179,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.NSLORD_PDNS_API + '/zones/' + response.data['name'] + '.')
 
         response.data['arecord'] = '10.13.3.7'
         response = self.client.put(url, response.data)
@@ -177,6 +196,8 @@ class AuthenticatedDomainTests(APITestCase):
 
 class AuthenticatedDynDomainTests(APITestCase):
     def setUp(self):
+        httpretty.reset()
+        httpretty.disable()
         if not hasattr(self, 'owner'):
             self.owner = utils.createUser(dyn=True)
             self.ownedDomains = [utils.createDomain(self.owner, dyn=True), utils.createDomain(self.owner, dyn=True)]
@@ -184,6 +205,39 @@ class AuthenticatedDynDomainTests(APITestCase):
             self.token = utils.createToken(user=self.owner)
             self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
 
+    def testCanDeleteOwnedDynDomain(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.ownedDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.ownedDomains[1].name+ '.')
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/dedyn.io.')
+
+        url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.assertEqual(httpretty.last_request().method, 'PATCH')
+        self.assertEqual(httpretty.last_request().headers['Host'], 'nslord:8081')
+        self.assertTrue('"NS"' in httpretty.last_request().parsed_body
+                        and '"' + self.ownedDomains[1].name + '."' in httpretty.last_request().parsed_body
+                        and '"DELETE"' in httpretty.last_request().parsed_body)
+
+        httpretty.reset()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.ownedDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.ownedDomains[1].name+ '.')
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertTrue(isinstance(httpretty.last_request(), httpretty.core.HTTPrettyRequestEmpty))
+
+    def testCantDeleteOtherDynDomains(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.DELETE, settings.NSLORD_PDNS_API + '/zones/' + self.otherDomains[1].name + '.')
+        httpretty.register_uri(httpretty.DELETE, settings.NSMASTER_PDNS_API + '/zones/' + self.otherDomains[1].name+ '.')
+
+        url = reverse('domain-detail', args=(self.otherDomains[1].pk,))
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertTrue(isinstance(httpretty.last_request(), httpretty.core.HTTPrettyRequestEmpty))
+
     def testCanPostDynDomains(self):
         url = reverse('domain-list')
         data = {'name': utils.generateDynDomainname()}
@@ -208,7 +262,7 @@ class AuthenticatedDynDomainTests(APITestCase):
 
     def testLimitDynDomains(self):
         httpretty.enable()
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
 
         outboxlen = len(mail.outbox)
 
@@ -226,7 +280,7 @@ class AuthenticatedDynDomainTests(APITestCase):
 
     def testCantUseInvalidCharactersInDomainNamePDNS(self):
         httpretty.enable()
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
 
         outboxlen = len(mail.outbox)
         invalidnames = [

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

@@ -31,8 +31,8 @@ class DynDNS12UpdateTest(APITestCase):
 
         httpretty.enable()
         httpretty.HTTPretty.allow_net_connect = False
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain + '.')
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
 
     def tearDown(self):
         httpretty.disable()
@@ -155,13 +155,13 @@ class DynDNS12UpdateTest(APITestCase):
         domain.arecord = '10.1.1.1'
         domain.save()
 
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain + '.')
-        httpretty.register_uri(httpretty.GET, settings.POWERDNS_API + '/zones/' + self.domain + '.', status=200)
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
+        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.', status=200)
 
         self.owner.unlock()
 
         self.assertEqual(httpretty.last_request().method, 'PATCH')
-        self.assertTrue((settings.POWERDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.last_request().path))
+        self.assertTrue((settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.last_request().path))
         self.assertTrue(self.domain in httpretty.last_request().parsed_body)
         self.assertTrue('10.1.1.1' in httpretty.last_request().parsed_body)
 
@@ -184,17 +184,17 @@ class DynDNS12UpdateTest(APITestCase):
         domain.arecord = '10.1.1.1'
         domain.save()
 
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + newdomain + '.')
-        httpretty.register_uri(httpretty.GET, settings.POWERDNS_API + '/zones/' + newdomain + '.', status=200)
-        httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + self.domain + '.')
-        httpretty.register_uri(httpretty.GET, settings.POWERDNS_API + '/zones/' + self.domain + '.', status=200)
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.')
+        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.', status=200)
+        httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
+        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.', status=200)
 
         self.owner.unlock()
 
         self.assertEqual(httpretty.last_request().method, 'PATCH')
         self.assertTrue(
-                (settings.POWERDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.last_request().path) \
-                or (settings.POWERDNS_API + '/zones/' + newdomain + '.').endswith(httpretty.last_request().path)
+                (settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.last_request().path) \
+                or (settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.').endswith(httpretty.last_request().path)
             )
         self.assertTrue('10.2.2.2' in httpretty.last_request().parsed_body)

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

@@ -30,8 +30,8 @@ class DynUpdateAuthenticationTests(APITestCase):
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
             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.POST, settings.NSLORD_PDNS_API + '/zones')
+            httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
 
     def testSuccessfulAuthentication(self):
         response = self.client.get(self.url)

+ 0 - 7
api/desecapi/views.py

@@ -91,13 +91,6 @@ class DomainDetail(generics.RetrieveUpdateDestroyAPIView):
     def get_queryset(self):
         return Domain.objects.filter(owner=self.request.user.pk)
 
-    def put(self, request, pk, format=None):
-        # Don't accept PUT requests for non-existent or non-owned domains.
-        domain = Domain.objects.filter(owner=self.request.user.pk, pk=pk)
-        if len(domain) is 0:
-            raise Http404
-        return super(DomainDetail, self).put(request, pk, format)
-
 
 class DomainDetailByName(DomainDetail):
     lookup_field = 'name'

+ 1 - 1
api/requirements.txt

@@ -9,7 +9,7 @@ djangorestframework==3.5.3
 djoser==0.5.1
 dnspython==1.15.0
 enum34==1.1.6
-httpretty==0.8.10
+httpretty==0.8.14
 idna==2.0
 ipaddress==1.0.7
 pyOpenSSL==0.15.1

+ 3 - 0
docker-compose.yml

@@ -110,6 +110,7 @@ services:
     - DESECSTACK_API_SECRETKEY
     - DESECSTACK_DBAPI_PASSWORD_desec
     - DESECSTACK_NSLORD_APIKEY
+    - DESECSTACK_NSMASTER_APIKEY
     - DESECSTACK_NORECAPTCHA_SITE_KEY
     - DESECSTACK_NORECAPTCHA_SECRET_KEY
     networks:
@@ -147,7 +148,9 @@ services:
     build: nsmaster
     image: desec/dedyn-nsmaster:latest
     environment:
+    - DESECSTACK_IPV4_REAR_PREFIX16
     - DESECSTACK_DBMASTER_PASSWORD_pdns
+    - DESECSTACK_NSMASTER_APIKEY
     - DESECSTACK_NSMASTER_CARBONSERVER
     - DESECSTACK_NSMASTER_CARBONOURNAME
     depends_on:

+ 5 - 0
nsmaster/conf/pdns.conf.var

@@ -1,10 +1,15 @@
 allow-unsigned-notify=yes
 allow-unsigned-supermaster=yes
+api=yes
+api-key=${DESECSTACK_NSMASTER_APIKEY}
 disable-axfr=yes
 setgid=pdns
 setuid=pdns
 slave=yes
 version-string=powerdns
+webserver=yes
+webserver-address=0.0.0.0
+webserver-allow-from=${DESECSTACK_IPV4_REAR_PREFIX16}.1.0/24
 carbon-server=${DESECSTACK_NSMASTER_CARBONSERVER}
 carbon-ourname=${DESECSTACK_NSMASTER_CARBONOURNAME}
 

+ 3 - 4
test-api

@@ -25,11 +25,10 @@ then
   sleep 60
 fi &
 
-if [ -z "$(docker volume ls | grep desecstack_nslord)" ]
+if [ -z "$(docker volume ls | grep 'desecstack_dblord')" ]
 then
-  echo "NO NSLORD FOUND, PREPARING DBLORD, DBAPI"
-  docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.test.yml build dblord
-  docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.test.yml build nslord
+  echo "NO NSLORD/NSMASTER FOUND, PREPARING"
+  docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.test.yml build dblord nslord
   docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.test.yml up -d dblord
   sleep 60
   docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.test.yml up -d nslord