diff options
author | Jenkins <jenkins@review.openstack.org> | 2013-10-11 12:21:06 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-10-11 12:21:06 +0000 |
commit | 361cd7af637870627ae399a98aff0201d6a3babb (patch) | |
tree | 4f85f5a6c7d0b87c3094bd5fedf0fd811f91c4e4 | |
parent | 94a7bbe417ad00368bbf4cb3b028e03e0617dca0 (diff) | |
parent | f2c8658b944ca75832e9e1e02fa2caec5e568231 (diff) | |
download | designate-361cd7af637870627ae399a98aff0201d6a3babb.tar.gz |
Merge "Update domains when servers are created, modified or deleted"
-rw-r--r-- | designate/backend/base.py | 12 | ||||
-rw-r--r-- | designate/backend/impl_bind9.py | 22 | ||||
-rw-r--r-- | designate/backend/impl_dnsmasq.py | 14 | ||||
-rw-r--r-- | designate/backend/impl_fake.py | 9 | ||||
-rw-r--r-- | designate/backend/impl_mysqlbind9.py | 52 | ||||
-rw-r--r-- | designate/backend/impl_powerdns/__init__.py | 180 | ||||
-rw-r--r-- | designate/backend/impl_rpc.py | 9 | ||||
-rw-r--r-- | designate/central/service.py | 21 | ||||
-rw-r--r-- | designate/exceptions.py | 4 | ||||
-rw-r--r-- | designate/tests/test_api/test_v1/test_servers.py | 12 | ||||
-rw-r--r-- | designate/tests/test_central/test_service.py | 9 |
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() |