summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-10-11 12:21:06 +0000
committerGerrit Code Review <review@openstack.org>2013-10-11 12:21:06 +0000
commit361cd7af637870627ae399a98aff0201d6a3babb (patch)
tree4f85f5a6c7d0b87c3094bd5fedf0fd811f91c4e4
parent94a7bbe417ad00368bbf4cb3b028e03e0617dca0 (diff)
parentf2c8658b944ca75832e9e1e02fa2caec5e568231 (diff)
downloaddesignate-361cd7af637870627ae399a98aff0201d6a3babb.tar.gz
Merge "Update domains when servers are created, modified or deleted"
-rw-r--r--designate/backend/base.py12
-rw-r--r--designate/backend/impl_bind9.py22
-rw-r--r--designate/backend/impl_dnsmasq.py14
-rw-r--r--designate/backend/impl_fake.py9
-rw-r--r--designate/backend/impl_mysqlbind9.py52
-rw-r--r--designate/backend/impl_powerdns/__init__.py180
-rw-r--r--designate/backend/impl_rpc.py9
-rw-r--r--designate/central/service.py21
-rw-r--r--designate/exceptions.py4
-rw-r--r--designate/tests/test_api/test_v1/test_servers.py12
-rw-r--r--designate/tests/test_central/test_service.py9
11 files changed, 335 insertions, 9 deletions
diff --git a/designate/backend/base.py b/designate/backend/base.py
index 1fcc02c6..39d2a4c1 100644
--- a/designate/backend/base.py
+++ b/designate/backend/base.py
@@ -72,6 +72,18 @@ class Backend(Plugin):
def delete_record(self, context, domain, record):
""" Delete a DNS record """
+ @abc.abstractmethod
+ def create_server(self, context, server):
+ """ Create a DNS server """
+
+ @abc.abstractmethod
+ def update_server(self, context, server):
+ """ Update a DNS server """
+
+ @abc.abstractmethod
+ def delete_server(self, context, server):
+ """ Delete a DNS server """
+
def sync_domain(self, context, domain, records):
"""
Re-Sync a DNS domain
diff --git a/designate/backend/impl_bind9.py b/designate/backend/impl_bind9.py
index 6bf4b218..b65eb206 100644
--- a/designate/backend/impl_bind9.py
+++ b/designate/backend/impl_bind9.py
@@ -51,6 +51,18 @@ class Bind9Backend(base.Backend):
LOG.debug('Calling RNDC with: %s' % " ".join(rndc_call))
utils.execute(*rndc_call)
+ def create_server(self, context, server):
+ LOG.debug('Create Server')
+ self._sync_domains_on_server_change()
+
+ def update_server(self, context, server):
+ LOG.debug('Update Server')
+ self._sync_domains_on_server_change()
+
+ def delete_server(self, context, server):
+ LOG.debug('Delete Server')
+ self._sync_domains_on_server_change()
+
def create_domain(self, context, domain):
LOG.debug('Create Domain')
self._sync_domain(domain, new_domain_flag=True)
@@ -161,3 +173,13 @@ class Bind9Backend(base.Backend):
output_file = os.path.join(output_folder, 'zones.config')
shutil.copyfile(nzf_name[0], output_file)
+
+ def _sync_domains_on_server_change(self):
+ # TODO(eankutse): Improve this so it scales. Need to design
+ # for it in the new Pool Manager/Agent for the backend that is
+ # being proposed
+ LOG.debug('Synchronising domains on server change')
+
+ domains = self.central_service.find_domains(self.admin_context)
+ for domain in domains:
+ self._sync_domain(domain)
diff --git a/designate/backend/impl_dnsmasq.py b/designate/backend/impl_dnsmasq.py
index 4d3b3ab6..43489285 100644
--- a/designate/backend/impl_dnsmasq.py
+++ b/designate/backend/impl_dnsmasq.py
@@ -50,6 +50,20 @@ class DnsmasqBackend(base.Backend):
self._sync_domains()
+ # Since dnsmasq only supports A and AAAA records, create_server,
+ # update_server, and delete_server can be noop's
+ def create_server(self, context, server):
+ LOG.debug('Create Server - noop')
+ pass
+
+ def update_server(self, context, server):
+ LOG.debug('Update Server - noop')
+ pass
+
+ def delete_server(self, context, server):
+ LOG.debug('Delete Server - noop')
+ pass
+
def create_domain(self, context, domain):
LOG.debug('Create Domain')
diff --git a/designate/backend/impl_fake.py b/designate/backend/impl_fake.py
index 2e2f30a7..1e2ce76b 100644
--- a/designate/backend/impl_fake.py
+++ b/designate/backend/impl_fake.py
@@ -34,6 +34,15 @@ class FakeBackend(base.Backend):
def delete_tsigkey(self, context, tsigkey):
LOG.info('Delete TSIG Key %r' % tsigkey)
+ def create_server(self, context, server):
+ LOG.info('Create Server %r' % server)
+
+ def update_server(self, context, server):
+ LOG.debug('Update Server %r' % server)
+
+ def delete_server(self, context, server):
+ LOG.debug('Delete Server %r' % server)
+
def create_domain(self, context, domain):
LOG.info('Create Domain %r' % domain)
diff --git a/designate/backend/impl_mysqlbind9.py b/designate/backend/impl_mysqlbind9.py
index 585e31eb..62797b83 100644
--- a/designate/backend/impl_mysqlbind9.py
+++ b/designate/backend/impl_mysqlbind9.py
@@ -19,6 +19,7 @@ import os
from oslo.config import cfg
from designate.openstack.common import log as logging
from designate import utils
+from designate import exceptions
from designate.backend import base
from sqlalchemy.ext.sqlsoup import SqlSoup
from sqlalchemy.engine.url import _parse_rfc1738_args
@@ -270,6 +271,57 @@ class MySQLBind9Backend(base.Backend):
self._sync_domains()
+ def create_server(self, context, server):
+ LOG.debug('create_server()')
+
+ raise exceptions.NotImplemented('create_server() for '
+ 'mysqlbind9 backend is '
+ 'not implemented')
+
+ """
+ TODO: this first-cut will not scale. Use bulk SQLAlchemy (core) queries
+ if cfg.CONF[self.name].write_database:
+ domains = self.central_service.find_domains(self.admin_context)
+
+ for domain in domains:
+ self._add_ns_records(domain, server)
+
+ self._sync_domains()
+ """
+
+# This method could be a very expensive and should only be called
+# (e.g., from central) only if the name of the existing server is
+# changed.
+ def update_server(self, context, server):
+ LOG.debug('update_server()')
+
+ raise exceptions.NotImplemented('update_server() for '
+ 'mysqlbind9 backend is '
+ 'not implemented')
+
+ """
+ TODO: this first-cut will not scale. Use bulk SQLAlchemy (core) queries
+ if cfg.CONF[self.name].write_database:
+ servers = self.central_service.find_servers(self.admin_context)
+ domains = self.central_service.find_domains(self.admin_context)
+
+ for domain in domains:
+ self._update_ns_records(domain, servers)
+
+ self._sync_domains()
+ """
+
+ def delete_server(self, context, server):
+ LOG.debug('delete_server()')
+
+ raise exceptions.NotImplemented('delete_server() for '
+ 'mysqlbind9 backend is'
+ ' not implemented')
+
+ """
+ TODO: For scale, Use bulk SQLAlchemy (core) queries
+ """
+
def create_record(self, context, domain, record):
LOG.debug('create_record()')
if cfg.CONF[self.name].write_database:
diff --git a/designate/backend/impl_powerdns/__init__.py b/designate/backend/impl_powerdns/__init__.py
index 98b90d06..dca47b00 100644
--- a/designate/backend/impl_powerdns/__init__.py
+++ b/designate/backend/impl_powerdns/__init__.py
@@ -16,8 +16,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import base64
+from sqlalchemy import func
from sqlalchemy.sql import select
from sqlalchemy.sql.expression import null
+from sqlalchemy.sql.expression import and_
from sqlalchemy.orm import exc as sqlalchemy_exceptions
from oslo.config import cfg
from designate.openstack.common import excutils
@@ -122,6 +124,18 @@ class PowerDNSBackend(base.Backend):
.filter_by(kind='TSIG-ALLOW-AXFR', content=tsigkey['name'])\
.delete()
+ def create_server(self, context, server):
+ LOG.debug('Create Server')
+ self._update_domains_on_server_create(server)
+
+ def update_server(self, context, server):
+ LOG.debug('Update Server')
+ self._update_domains_on_server_update(server)
+
+ def delete_server(self, context, server):
+ LOG.debug('Delete Server')
+ self._update_domains_on_server_delete(server)
+
# Domain Methods
def create_domain(self, context, domain):
servers = self.central_service.find_servers(self.admin_context)
@@ -321,6 +335,9 @@ class PowerDNSBackend(base.Backend):
return content
+ def _sanitize_uuid_str(self, uuid):
+ return uuid.replace("-", "")
+
def _build_soa_content(self, domain, servers):
return "%s %s. %d %d %d %d %d" % (servers[0]['name'],
domain['email'].replace("@", "."),
@@ -374,3 +391,166 @@ class PowerDNSBackend(base.Backend):
raise exceptions.RecordNotFound('Too many records found')
else:
return record
+
+ def _update_domains_on_server_create(self, server):
+ """
+ For performance, manually prepare a bulk insert query to
+ build NS records for all existing domains for insertion
+ into Record table
+ """
+ ns_rec_content = self._sanitize_content("NS", server['name'])
+
+ LOG.debug("Content field of newly created NS records for "
+ "existing domains upon server create is: %s"
+ % ns_rec_content)
+
+ query_select = select([null(),
+ models.Domain.__table__.c.id,
+ models.Domain.__table__.c.name,
+ "'NS'",
+ "'%s'" % ns_rec_content,
+ null(),
+ null(),
+ null(),
+ null(),
+ 1,
+ "'%s'" % self._sanitize_uuid_str(server['id']),
+ 1])
+ query = InsertFromSelect(models.Record.__table__, query_select)
+
+ # Execute the manually prepared query
+ # A TX is required for, at the least, SQLite.
+ try:
+ self.session.begin()
+ self.session.execute(query)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self.session.rollback()
+ else:
+ self.session.commit()
+
+ def _update_domains_on_server_update(self, server):
+ """
+ For performance, manually prepare a bulk update query to
+ update all NS records for all existing domains that need
+ updating of their corresponding NS record in Record table
+ """
+ ns_rec_content = self._sanitize_content("NS", server['name'])
+
+ LOG.debug("Content field of existing NS records will be updated"
+ " to the following upon server update: %s" % ns_rec_content)
+ try:
+
+ # Execute the manually prepared query
+ # A TX is required for, at the least, SQLite.
+ #
+ self.session.begin()
+
+ # first determine the old name of the server
+ # before making the updates. Since the value
+ # is coming from an NS record, the server name
+ # will not have a trailing period (.)
+ old_ns_rec = self.session.query(models.Record)\
+ .filter_by(type='NS', designate_id=server['id'])\
+ .first()
+ old_server_name = old_ns_rec.content
+
+ LOG.debug("old server name read from a backend NS record:"
+ " %s" % old_server_name)
+ LOG.debug("new server name: %s" % server['name'])
+
+ # Then update all NS records that need updating
+ # Only the name of a server has changed when we are here
+ self.session.query(models.Record)\
+ .filter_by(type='NS', designate_id=server['id'])\
+ .update({"content": ns_rec_content})
+
+ # Then update all SOA records as necessary
+ # Do the SOA last, ensuring we don't trigger a NOTIFY
+ # before the NS records are in place.
+ #
+ # Update the content field of every SOA record that has the
+ # old server name as part of its 'content' field to reflect
+ # the new server name.
+ # Need to strip the trailing period from the server['name']
+ # before using it to replace the old_server_name in the SOA
+ # record since the SOA record already has a trailing period
+ # and we want to keep it
+ self.session.execute(models.Record.__table__
+ .update()
+ .where(and_(models.Record.__table__.c.type == "SOA",
+ models.Record.__table__.c.content.like
+ ("%s%%" % old_server_name)))
+ .values(content=
+ func.replace(
+ models.Record.__table__.c.content,
+ old_server_name,
+ server['name'].rstrip('.'))
+ )
+ )
+
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self.session.rollback()
+ # now commit
+ else:
+ self.session.commit()
+
+ def _update_domains_on_server_delete(self, server):
+ """
+ For performance, manually prepare a bulk update query to
+ update all NS records for all existing domains that need
+ updating of their corresponding NS record in Record table
+ """
+
+ # find a replacement server
+ replacement_server_name = None
+ servers = self.central_service.find_servers(self.admin_context)
+
+ for replacement in servers:
+ if replacement['id'] != server['id']:
+ replacement_server_name = replacement['name']
+ break
+
+ LOG.debug("This existing server name will be used to update existing"
+ " SOA records upon server delete: %s "
+ % replacement_server_name)
+
+ # NOTE: because replacement_server_name came from central storage
+ # it has the trailing period
+
+ # Execute the manually prepared query
+ # A TX is required for, at the least, SQLite.
+ try:
+ self.session.begin()
+ # first delete affected NS records
+ self.session.query(models.Record)\
+ .filter_by(type='NS', designate_id=server['id'])\
+ .delete()
+
+ # then update all SOA records as necessary
+ # Do the SOA last, ensuring we don't trigger a
+ # NOTIFY before the NS records are in place.
+ #
+ # Update the content field of every SOA record that
+ # has the deleted server name as part of its
+ # 'content' field to reflect the name of another
+ # server that exists
+ # both server['name'] and replacement_server_name
+ # have trailing period so we are fine just doing the
+ # substitution without striping trailing period
+ self.session.execute(models.Record.__table__
+ .update()
+ .where(and_(models.Record.__table__.c.type == "SOA",
+ models.Record.__table__.c.content.like
+ ("%s%%" % server['name'])))
+ .values(content=func.replace(
+ models.Record.__table__.c.content,
+ server['name'],
+ replacement_server_name)))
+
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self.session.rollback()
+ else:
+ self.session.commit()
diff --git a/designate/backend/impl_rpc.py b/designate/backend/impl_rpc.py
index b02190f6..4dae6fcd 100644
--- a/designate/backend/impl_rpc.py
+++ b/designate/backend/impl_rpc.py
@@ -29,6 +29,15 @@ class RPCBackend(base.Backend):
def delete_tsigkey(self, context, tsigkey):
return agent_api.delete_tsigkey(context, tsigkey)
+ def create_server(self, context, server):
+ return agent_api.create_server(context, server)
+
+ def update_server(self, context, server):
+ return agent_api.update_server(context, server)
+
+ def delete_server(self, context, server):
+ return agent_api.delete_server(context, server)
+
def create_domain(self, context, domain):
return agent_api.create_domain(context, domain)
diff --git a/designate/central/service.py b/designate/central/service.py
index 4a5ae9ee..205d8c57 100644
--- a/designate/central/service.py
+++ b/designate/central/service.py
@@ -273,8 +273,9 @@ class Service(rpc_service.Service):
policy.check('create_server', context)
with self.storage_api.create_server(context, values) as server:
- # TODO(kiall): Update backend with the new server..
- pass
+ # Update backend with the new server..
+ with wrap_backend_call():
+ self.backend.create_server(context, server)
utils.notify(context, 'central', 'server.create', server)
@@ -295,8 +296,9 @@ class Service(rpc_service.Service):
with self.storage_api.update_server(
context, server_id, values) as server:
- # TODO(kiall): Update backend with the new details..
- pass
+ # Update backend with the new details..
+ with wrap_backend_call():
+ self.backend.update_server(context, server)
utils.notify(context, 'central', 'server.update', server)
@@ -305,9 +307,16 @@ class Service(rpc_service.Service):
def delete_server(self, context, server_id):
policy.check('delete_server', context, {'server_id': server_id})
+ # don't delete last of servers
+ servers = self.storage_api.find_servers(context)
+ if len(servers) == 1 and server_id == servers[0]['id']:
+ raise exceptions.LastServerDeleteNotAllowed(
+ "Not allowed to delete last of servers")
+
with self.storage_api.delete_server(context, server_id) as server:
- # TODO(kiall): Update backend with the new server..
- pass
+ # Update backend with the new server..
+ with wrap_backend_call():
+ self.backend.delete_server(context, server)
utils.notify(context, 'central', 'server.delete', server)
diff --git a/designate/exceptions.py b/designate/exceptions.py
index 696c53e3..04cb0b32 100644
--- a/designate/exceptions.py
+++ b/designate/exceptions.py
@@ -156,6 +156,10 @@ class RecordNotFound(NotFound):
error_type = 'record_not_found'
+class LastServerDeleteNotAllowed(BadRequest):
+ error_type = 'last_server_delete_not_allowed'
+
+
class ResourceNotFound(NotFound):
# TODO(kiall): Should this be extending NotFound??
pass
diff --git a/designate/tests/test_api/test_v1/test_servers.py b/designate/tests/test_api/test_v1/test_servers.py
index a2e3c02c..8b3334c3 100644
--- a/designate/tests/test_api/test_v1/test_servers.py
+++ b/designate/tests/test_api/test_v1/test_servers.py
@@ -199,11 +199,19 @@ class ApiV1ServersTest(ApiV1Test):
# Create a server
server = self.create_server()
+ # Create a second server so that we can delete the first
+ # because the last remaining server is not allowed to be deleted
+ server2 = self.create_server(fixture=1)
+
+ # Now delete the server
self.delete('servers/%s' % server['id'])
- # Ensure we can no longer fetch the server
+ # Ensure we can no longer fetch the deleted server
self.get('servers/%s' % server['id'], status_code=404)
+ # Also, verify we cannot delete last remaining server
+ self.delete('servers/%s' % server2['id'], status_code=400)
+
@patch.object(central_service.Service, 'delete_server')
def test_delete_server_trailing_slash(self, mock):
# Create a server
@@ -223,5 +231,5 @@ class ApiV1ServersTest(ApiV1Test):
self.delete('servers/%s' % server['id'], status_code=504)
def test_delete_server_missing(self):
- self.delete('servers/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ self.delete('servers/9fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
status_code=404)
diff --git a/designate/tests/test_central/test_service.py b/designate/tests/test_central/test_service.py
index 215fa025..4152b636 100644
--- a/designate/tests/test_central/test_service.py
+++ b/designate/tests/test_central/test_service.py
@@ -194,13 +194,20 @@ class CentralServiceTest(CentralTestCase):
# Create a server
server = self.create_server()
- # Delete the server
+ # Create a second server
+ server2 = self.create_server(fixture=1)
+
+ # Delete one server
self.central_service.delete_server(context, server['id'])
# Fetch the server again, ensuring an exception is raised
with self.assertRaises(exceptions.ServerNotFound):
self.central_service.get_server(context, server['id'])
+ # Try to delete last remaining server - expect exception
+ with self.assertRaises(exceptions.LastServerDeleteNotAllowed):
+ self.central_service.delete_server(context, server2['id'])
+
# TsigKey Tests
def test_create_tsigkey(self):
context = self.get_admin_context()