diff options
author | Endre Karlson <endre.karlson@hp.com> | 2014-09-06 15:27:11 +0200 |
---|---|---|
committer | Kiall Mac Innes <kiall@hp.com> | 2014-09-25 14:07:53 +0100 |
commit | fbfeec3d12229eede252b45fbceabd04f507dc21 (patch) | |
tree | 2af4410b75363e6af34ac421e9f6bf566061e6c6 | |
parent | 8817b209d22099a6e73db9ac1b16d6a968f44680 (diff) | |
download | designate-fbfeec3d12229eede252b45fbceabd04f507dc21.tar.gz |
Make use of SQLA Core in PowerDNS
Closes-Bug: 1368863
Change-Id: I2534a84a62c9e438d8091ec2a6d959a6960c6a95
-rw-r--r-- | designate/backend/impl_powerdns/__init__.py | 310 | ||||
-rw-r--r-- | designate/backend/impl_powerdns/models.py | 78 | ||||
-rw-r--r-- | designate/backend/impl_powerdns/tables.py | 80 | ||||
-rw-r--r-- | designate/exceptions.py | 5 |
4 files changed, 281 insertions, 192 deletions
diff --git a/designate/backend/impl_powerdns/__init__.py b/designate/backend/impl_powerdns/__init__.py index 4aa253eb..371ef0bc 100644 --- a/designate/backend/impl_powerdns/__init__.py +++ b/designate/backend/impl_powerdns/__init__.py @@ -21,14 +21,13 @@ import threading from oslo.config import cfg from oslo.db import options from sqlalchemy.sql import select -from sqlalchemy.orm import exc as sqlalchemy_exceptions from designate.openstack.common import excutils from designate.openstack.common import log as logging from designate.i18n import _LC from designate import exceptions from designate.backend import base -from designate.backend.impl_powerdns import models +from designate.backend.impl_powerdns import tables from designate.sqlalchemy import session from designate.sqlalchemy.expressions import InsertFromSelect @@ -53,6 +52,10 @@ CONF.set_default('connection', 'sqlite:///$state_path/powerdns.sqlite', group='backend:powerdns') +def _map_col(keys, col): + return dict([(keys[i], col[i]) for i in range(len(keys))]) + + class PowerDNSBackend(base.Backend): __plugin_name__ = 'powerdns' @@ -77,6 +80,67 @@ class PowerDNSBackend(base.Backend): return self.local_store.session + def _create(self, table, values): + query = table.insert() + + resultproxy = self.session.execute(query, values) + + # Refetch the row, for generated columns etc + query = select([table])\ + .where(table.c.id == resultproxy.inserted_primary_key[0]) + resultproxy = self.session.execute(query) + + return _map_col(query.columns.keys(), resultproxy.fetchone()) + + def _update(self, table, values, exc_notfound, id_col=None): + if id_col is None: + id_col = table.c.id + + query = table.update()\ + .where(id_col == values[id_col.name])\ + .values(**values) + + resultproxy = self.session.execute(query) + + if resultproxy.rowcount != 1: + raise exc_notfound() + + # Refetch the row, for generated columns etc + query = select([table])\ + .where(id_col == values[id_col.name]) + resultproxy = self.session.execute(query) + + return _map_col(query.columns.keys(), resultproxy.fetchone()) + + def _get(self, table, id_, exc_notfound, id_col=None): + if id_col is None: + id_col = table.c.id + + query = select([table])\ + .where(id_col == id_) + + resultproxy = self.session.execute(query) + + results = resultproxy.fetchall() + + if len(results) != 1: + raise exc_notfound() + + # Map col keys to values in result + return _map_col(query.columns.keys(), results[0]) + + def _delete(self, table, id_, exc_notfound, id_col=None): + if id_col is None: + id_col = table.c.id + + query = table.delete()\ + .where(id_col == id_) + + resultproxy = self.session.execute(query) + + if resultproxy.rowcount != 1: + raise exc_notfound() + # TSIG Key Methods def create_tsigkey(self, context, tsigkey): """Create a TSIG Key""" @@ -84,33 +148,31 @@ class PowerDNSBackend(base.Backend): if tsigkey['algorithm'] not in TSIG_SUPPORTED_ALGORITHMS: raise exceptions.NotImplemented('Unsupported algorithm') - tsigkey_m = models.TsigKey() - - tsigkey_m.update({ + values = { 'designate_id': tsigkey['id'], 'name': tsigkey['name'], 'algorithm': tsigkey['algorithm'], 'secret': base64.b64encode(tsigkey['secret']) - }) + } - tsigkey_m.save(self.session) + self._create(tables.tsigkeys, values) # NOTE(kiall): Prepare and execute query to install this TSIG Key on # every domain. We use a manual query here since anything # else would be impossibly slow. query_select = select([ - models.Domain.__table__.c.id, + tables.domains.c.id, "'TSIG-ALLOW-AXFR'", "'%s'" % tsigkey['name']] ) columns = [ - models.DomainMetadata.__table__.c.domain_id, - models.DomainMetadata.__table__.c.kind, - models.DomainMetadata.__table__.c.content, + tables.domain_metadata.c.domain_id, + tables.domain_metadata.c.kind, + tables.domain_metadata.c.content, ] - query = InsertFromSelect(models.DomainMetadata.__table__, query_select, + query = InsertFromSelect(tables.domain_metadata, query_select, columns) # NOTE(kiall): A TX is required for, at the least, SQLite. @@ -120,31 +182,42 @@ class PowerDNSBackend(base.Backend): def update_tsigkey(self, context, tsigkey): """Update a TSIG Key""" - tsigkey_m = self._get_tsigkey(tsigkey['id']) + values = self._get( + tables.tsigkeys, + tsigkey['id'], + exceptions.TsigKeyNotFound, + id_col=tables.tsigkeys.c.designate_id) # Store a copy of the original name.. - original_name = tsigkey_m.name + original_name = values['name'] - tsigkey_m.update({ + values.update({ 'name': tsigkey['name'], 'algorithm': tsigkey['algorithm'], 'secret': base64.b64encode(tsigkey['secret']) }) - tsigkey_m.save(self.session) + self._update(tables.tsigkeys, values, + id_col=tables.tsigkeys.c.designate_id, + exc_notfound=exceptions.TsigKeyNotFound) # If the name changed, Update the necessary DomainMetadata records if original_name != tsigkey['name']: - self.session.query(models.DomainMetadata)\ - .filter_by(kind='TSIG-ALLOW-AXFR', content=original_name)\ - .update(content=tsigkey['name']) + query = tables.domain_metadata.update()\ + .where(tables.domain_metadata.c.kind == 'TSIG_ALLOW_AXFR')\ + .where(tables.domain_metadata.c.content == original_name) + + query.values(content=tsigkey['name']) + self.session.execute(query) def delete_tsigkey(self, context, tsigkey): """Delete a TSIG Key""" try: # Delete this TSIG Key itself - tsigkey_m = self._get_tsigkey(tsigkey['id']) - tsigkey_m.delete(self.session) + self._delete( + tables.tsigkeys, tsigkey['id'], + exceptions.TsigKeyNotFound, + id_col=tables.tsigkeys.c.designate_id) except exceptions.TsigKeyNotFound: # If the TSIG Key is already gone, that's ok. We're deleting it # anyway, so just log and continue. @@ -153,46 +226,58 @@ class PowerDNSBackend(base.Backend): tsigkey['id']) return - # Delete this TSIG Key from every domain's metadata - self.session.query(models.DomainMetadata)\ - .filter_by(kind='TSIG-ALLOW-AXFR', content=tsigkey['name'])\ - .delete() + query = tables.domain_metadata.delete()\ + .where(tables.domain_metadata.c.kind == 'TSIG-ALLOW-AXFR')\ + .where(tables.domain_metadata.c.content == tsigkey['name']) + self.session.execute(query) # Domain Methods def create_domain(self, context, domain): - servers = self.central_service.find_servers(self.admin_context) - - domain_m = models.Domain() - domain_m.update({ - 'designate_id': domain['id'], - 'name': domain['name'].rstrip('.'), - 'master': servers[0]['name'].rstrip('.'), - 'type': CONF['backend:powerdns'].domain_type, - 'account': context.tenant - }) - domain_m.save(self.session) + try: + self.session.begin() + servers = self.central_service.find_servers(self.admin_context) + + domain_values = { + 'designate_id': domain['id'], + 'name': domain['name'].rstrip('.'), + 'master': servers[0]['name'].rstrip('.'), + 'type': CONF['backend:powerdns'].domain_type, + 'account': context.tenant + } - # Install all TSIG Keys on this domain - tsigkeys = self.session.query(models.TsigKey).all() - values = [t.name for t in tsigkeys] + domain_ref = self._create(tables.domains, domain_values) - self._update_domainmetadata(domain_m.id, 'TSIG-ALLOW-AXFR', values) + # Install all TSIG Keys on this domain + query = select([tables.tsigkeys.c.name]) + resultproxy = self.session.execute(query) + values = [i for i in resultproxy.fetchall()] - # Install all Also Notify's on this domain - self._update_domainmetadata(domain_m.id, 'ALSO-NOTIFY', - CONF['backend:powerdns'].also_notify) + self._update_domainmetadata(domain_ref['id'], 'TSIG-ALLOW-AXFR', + values) + + # Install all Also Notify's on this domain + self._update_domainmetadata(domain_ref['id'], 'ALSO-NOTIFY', + CONF['backend:powerdns'].also_notify) + except Exception: + with excutils.save_and_reraise_exception(): + self.session.rollback() + else: + self.session.commit() def update_domain(self, context, domain): - domain_m = self._get_domain(domain['id']) + domain_ref = self._get(tables.domains, domain['id'], + exceptions.DomainNotFound, + id_col=tables.domains.c.designate_id) try: self.session.begin() # Update the Records TTLs where necessary - self.session.query(models.Record)\ - .filter_by(domain_id=domain_m.id, inherit_ttl=True)\ - .update({'ttl': domain['ttl']}) - + query = tables.records.update()\ + .where(tables.records.c.domain_id == domain_ref['id']) + query = query.where(tables.records.c.inherit_ttl == True) # noqa\ + query = query.values(ttl=domain['ttl']) + self.session.execute(query) except Exception: with excutils.save_and_reraise_exception(): self.session.rollback() @@ -201,7 +286,9 @@ class PowerDNSBackend(base.Backend): def delete_domain(self, context, domain): try: - domain_m = self._get_domain(domain['id']) + domain_ref = self._get(tables.domains, domain['id'], + exceptions.DomainNotFound, + id_col=tables.domains.c.designate_id) except exceptions.DomainNotFound: # If the Domain is already gone, that's ok. We're deleting it # anyway, so just log and continue. @@ -210,15 +297,19 @@ class PowerDNSBackend(base.Backend): domain['id']) return - domain_m.delete(self.session) + self._delete(tables.domains, domain['id'], + exceptions.DomainNotFound, + id_col=tables.domains.c.designate_id) # Ensure the records are deleted - query = self.session.query(models.Record) - query.filter_by(domain_id=domain_m.id).delete() + query = tables.records.delete()\ + .where(tables.records.c.domain_id == domain_ref['id']) + self.session.execute(query) # Ensure domainmetadata is deleted - query = self.session.query(models.DomainMetadata) - query.filter_by(domain_id=domain_m.id).delete() + query = tables.domain_metadata.delete()\ + .where(tables.domain_metadata.c.domain_id == domain_ref['id']) + self.session.execute(query) # RecordSet Methods def create_recordset(self, context, domain, recordset): @@ -251,21 +342,23 @@ class PowerDNSBackend(base.Backend): def delete_recordset(self, context, domain, recordset): # Ensure records are deleted - query = self.session.query(models.Record) - query.filter_by(designate_recordset_id=recordset['id']).delete() + query = tables.records.delete()\ + .where(tables.records.c.designate_recordset_id == recordset['id']) + self.session.execute(query) # Record Methods def create_record(self, context, domain, recordset, record): - domain_m = self._get_domain(domain['id']) - record_m = models.Record() + domain_ref = self._get(tables.domains, domain['id'], + exceptions.DomainNotFound, + id_col=tables.domains.c.designate_id) content = self._sanitize_content(recordset['type'], record['data']) ttl = domain['ttl'] if recordset['ttl'] is None else recordset['ttl'] - record_m.update({ + record_values = { 'designate_id': record['id'], 'designate_recordset_id': record['recordset_id'], - 'domain_id': domain_m.id, + 'domain_id': domain_ref['id'], 'name': recordset['name'].rstrip('.'), 'type': recordset['type'], 'content': content, @@ -273,17 +366,17 @@ class PowerDNSBackend(base.Backend): 'inherit_ttl': True if recordset['ttl'] is None else False, 'prio': record['priority'], 'auth': self._is_authoritative(domain, recordset, record) - }) + } - record_m.save(self.session) + self._create(tables.records, record_values) def update_record(self, context, domain, recordset, record): - record_m = self._get_record(record['id']) + record_ref = self._get_record(record['id']) content = self._sanitize_content(recordset['type'], record['data']) ttl = domain['ttl'] if recordset['ttl'] is None else recordset['ttl'] - record_m.update({ + record_ref.update({ 'content': content, 'ttl': ttl, 'inherit_ttl': True if recordset['ttl'] is None else False, @@ -291,11 +384,14 @@ class PowerDNSBackend(base.Backend): 'auth': self._is_authoritative(domain, recordset, record) }) - record_m.save(self.session) + self._update(tables.records, record_ref, + exc_notfound=exceptions.RecordNotFound) def delete_record(self, context, domain, recordset, record): try: - record_m = self._get_record(record['id']) + record_ref = self._get(tables.records, record['id'], + exceptions.RecordNotFound, + id_col=tables.records.c.designate_id) except exceptions.RecordNotFound: # If the Record is already gone, that's ok. We're deleting it # anyway, so just log and continue. @@ -303,7 +399,8 @@ class PowerDNSBackend(base.Backend): 'not present in the backend. ID: %s') % record['id']) else: - record_m.delete(self.session) + self._delete(tables.records, record_ref['id'], + exceptions.RecordNotFound) # Internal Methods def _update_domainmetadata(self, domain_id, kind, values=None, @@ -312,29 +409,37 @@ class PowerDNSBackend(base.Backend): # Fetch all current metadata of the specified kind values = values or [] - query = self.session.query(models.DomainMetadata) - query = query.filter_by(domain_id=domain_id, kind=kind) + query = select([tables.domain_metadata.c.content])\ + .where(tables.domain_metadata.c.domain_id == domain_id)\ + .where(tables.domain_metadata.c.kind == kind) + resultproxy = self.session.execute(query) + results = resultproxy.fetchall() - metadatas = query.all() - - for metadata in metadatas: - if metadata.content not in values: + for metadata_id, content in results: + if content not in values: if delete: LOG.debug('Deleting stale domain metadata: %r' % - ([domain_id, kind, metadata.value],)) + ([domain_id, kind, content],)) # Delete no longer necessary values - metadata.delete(self.session) + # We should never get a notfound here, so UnknownFailure is + # a reasonable choice. + self._delete(tables.domain_metadata, metadata_id, + exceptions.UnknownFailure) else: # Remove pre-existing values from the list of values to insert - values.remove(metadata.content) + values.remove(content) # Insert new values for value in values: LOG.debug('Inserting new domain metadata: %r' % ([domain_id, kind, value],)) - m = models.DomainMetadata(domain_id=domain_id, kind=kind, - content=value) - m.save(self.session) + self._create( + tables.domain_metadata, + { + "domain_id": domain_id, + "kind": kind, + "content": value + }) def _is_authoritative(self, domain, recordset, record): # NOTE(kiall): See http://doc.powerdns.com/dnssec-modes.html @@ -352,47 +457,24 @@ class PowerDNSBackend(base.Backend): return content - def _get_tsigkey(self, tsigkey_id): - query = self.session.query(models.TsigKey) - - try: - tsigkey = query.filter_by(designate_id=tsigkey_id).one() - except sqlalchemy_exceptions.NoResultFound: - raise exceptions.TsigKeyNotFound('No tsigkey found') - except sqlalchemy_exceptions.MultipleResultsFound: - raise exceptions.TsigKeyNotFound('Too many tsigkeys found') - else: - return tsigkey - - def _get_domain(self, domain_id): - query = self.session.query(models.Domain) - - try: - domain = query.filter_by(designate_id=domain_id).one() - except sqlalchemy_exceptions.NoResultFound: - raise exceptions.DomainNotFound('No domain found') - except sqlalchemy_exceptions.MultipleResultsFound: - raise exceptions.DomainNotFound('Too many domains found') - else: - return domain - - def _get_record(self, record_id=None, domain=None, type=None): - query = self.session.query(models.Record) + def _get_record(self, record_id=None, domain=None, type_=None): + query = select([tables.records]) if record_id: - query = query.filter_by(designate_id=record_id) + query = query.where(tables.records.c.designate_id == record_id) - if type: - query = query.filter_by(type=type) + if type_: + query = query.where(tables.records.c.type == type_) if domain: - query = query.filter_by(domain_id=domain.id) + query = query.where(tables.records.c.domain_id == domain['id']) - try: - record = query.one() - except sqlalchemy_exceptions.NoResultFound: + resultproxy = self.session.execute(query) + results = resultproxy.fetchall() + + if len(results) < 1: raise exceptions.RecordNotFound('No record found') - except sqlalchemy_exceptions.MultipleResultsFound: + elif len(results) > 1: raise exceptions.RecordNotFound('Too many records found') else: - return record + return _map_col(query.columns.keys(), results[0]) diff --git a/designate/backend/impl_powerdns/models.py b/designate/backend/impl_powerdns/models.py deleted file mode 100644 index 4485ae1e..00000000 --- a/designate/backend/impl_powerdns/models.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2012 Hewlett-Packard Development Company, L.P. All Rights Reserved. -# Copyright 2012 Managed I.T. -# -# Author: Patrick Galbraith <patg@hp.com> -# Author: Kiall Mac Innes <kiall@managedit.ie> -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from sqlalchemy import Column, String, Text, Integer, Boolean -from sqlalchemy.ext.declarative import declarative_base - -from designate.sqlalchemy import models -from designate.sqlalchemy.types import UUID - - -class Base(models.Base): - id = Column(Integer, primary_key=True, autoincrement=True) - - -Base = declarative_base(cls=Base) - - -class TsigKey(Base): - __tablename__ = 'tsigkeys' - - designate_id = Column(UUID, nullable=False) - - name = Column(String(255), default=None, nullable=True) - algorithm = Column(String(255), default=None, nullable=True) - secret = Column(String(255), default=None, nullable=True) - - -class DomainMetadata(Base): - __tablename__ = 'domainmetadata' - - domain_id = Column(Integer(), nullable=False) - kind = Column(String(16), default=None, nullable=True) - content = Column(Text()) - - -class Domain(Base): - __tablename__ = 'domains' - - designate_id = Column(UUID, nullable=False) - - name = Column(String(255), nullable=False, unique=True) - master = Column(String(255), nullable=True) - last_check = Column(Integer, default=None, nullable=True) - type = Column(String(6), nullable=False) - notified_serial = Column(Integer, default=None, nullable=True) - account = Column(String(40), default=None, nullable=True) - - -class Record(Base): - __tablename__ = 'records' - - designate_id = Column(UUID, nullable=False) - designate_recordset_id = Column(UUID, default=None, nullable=True) - - domain_id = Column(Integer, default=None, nullable=True) - name = Column(String(255), default=None, nullable=True) - type = Column(String(10), default=None, nullable=True) - content = Column(Text, default=None, nullable=True) - ttl = Column(Integer, default=None, nullable=True) - prio = Column(Integer, default=None, nullable=True) - change_date = Column(Integer, default=None, nullable=True) - ordername = Column(String(255), default=None, nullable=True) - auth = Column(Boolean(), default=None, nullable=True) - inherit_ttl = Column(Boolean(), default=True) diff --git a/designate/backend/impl_powerdns/tables.py b/designate/backend/impl_powerdns/tables.py new file mode 100644 index 00000000..7e5a3b39 --- /dev/null +++ b/designate/backend/impl_powerdns/tables.py @@ -0,0 +1,80 @@ +# Copyright 2012-2014 Hewlett-Packard Development Company, L.P. +# +# Author: Kiall Mac Innes <kiall@hp.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from sqlalchemy import MetaData, Table, Column, String, Text, Integer, Boolean + + +from oslo.config import cfg + +from designate.sqlalchemy.types import UUID + +CONF = cfg.CONF + + +metadata = MetaData() + +tsigkeys = Table( + 'tsigkeys', metadata, + Column('id', Integer(), primary_key=True, autoincrement=True), + + Column('designate_id', UUID(), nullable=False), + Column('name', String(255), default=None, nullable=True), + Column('algorithm', String(255), default=None, nullable=True), + Column('secret', String(255), default=None, nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + +domain_metadata = Table( + 'domainmetadata', metadata, + Column('id', Integer(), primary_key=True, autoincrement=True), + + Column('domain_id', Integer(), nullable=False), + Column('kind', String(16), default=None, nullable=True), + Column('content', Text()), + mysql_engine='InnoDB', + mysql_charset='utf8') + +domains = Table( + 'domains', metadata, + Column('id', Integer, primary_key=True, autoincrement=True), + + Column('designate_id', UUID(), nullable=False), + Column('name', String(255), nullable=False, unique=True), + Column('master', String(255), nullable=True), + Column('last_check', Integer(), default=None, nullable=True), + Column('type', String(6), nullable=False), + Column('notified_serial', Integer(), default=None, nullable=True), + Column('account', String(40), default=None, nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + +records = Table( + 'records', metadata, + Column('id', Integer, primary_key=True, autoincrement=True), + + Column('designate_id', UUID(), nullable=False), + Column('designate_recordset_id', UUID(), default=None, nullable=True), + Column('domain_id', Integer(), default=None, nullable=True), + Column('name', String(255), default=None, nullable=True), + Column('type', String(10), default=None, nullable=True), + Column('content', Text(), default=None, nullable=True), + Column('ttl', Integer(), default=None, nullable=True), + Column('prio', Integer(), default=None, nullable=True), + Column('change_date', Integer(), default=None, nullable=True), + Column('ordername', String(255), default=None, nullable=True), + Column('auth', Boolean(), default=None, nullable=True), + Column('inherit_ttl', Boolean(), default=True), + mysql_engine='InnoDB', + mysql_charset='utf8') diff --git a/designate/exceptions.py b/designate/exceptions.py index e1029e58..97d17fa9 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -46,6 +46,11 @@ class ConfigurationError(Base): error_type = 'configuration_error' +class UnknownFailure(Base): + error_code = 500 + error_type = 'unknown_failure' + + class CommunicationFailure(Base): error_code = 504 error_type = 'communication_failure' |