Add support for TSIG to PowerDNS backend

Change-Id: I7c1a8c452706d8d482580520ec8ecef2e0400bc8
This commit is contained in:
Kiall Mac Innes 2013-02-11 22:37:25 +00:00
parent 49b491f06c
commit d258d8a844
6 changed files with 210 additions and 4 deletions

View File

@ -15,6 +15,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import base64
from sqlalchemy.sql import select
from sqlalchemy.sql.expression import null
from sqlalchemy.orm import exc as sqlalchemy_exceptions
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
@ -22,6 +25,7 @@ from moniker import exceptions
from moniker.backend import base
from moniker.backend.impl_powerdns import models
from moniker.sqlalchemy.session import get_session, SQLOPTS
from moniker.sqlalchemy.expressions import InsertFromSelect
LOG = logging.getLogger(__name__)
@ -40,8 +44,69 @@ class PowerDNSBackend(base.Backend):
self.session = get_session(self.name)
# TSIG Key Methods
def create_tsigkey(self, context, tsigkey):
""" Create a TSIG Key """
tsigkey_m = models.TsigKey()
tsigkey_m.update({
'moniker_id': tsigkey['id'],
'name': tsigkey['name'],
'algorithm': tsigkey['algorithm'],
'secret': base64.b64encode(tsigkey['secret'])
})
tsigkey_m.save(self.session)
# 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([null(),
models.Domain.__table__.c.id,
"'TSIG-ALLOW-AXFR'",
"'%s'" % tsigkey['name']])
query = InsertFromSelect(models.DomainMetadata.__table__, query_select)
# NOTE(kiall): A TX is required for, at the least, SQLite.
self.session.begin()
self.session.execute(query)
self.session.commit()
def update_tsigkey(self, context, tsigkey):
""" Update a TSIG Key """
tsigkey_m = self._get_tsigkey(tsigkey['id'])
# Store a copy of the original name..
original_name = tsigkey_m.name
tsigkey_m.update({
'name': tsigkey['name'],
'algorithm': tsigkey['algorithm'],
'secret': base64.b64encode(tsigkey['secret'])
})
tsigkey_m.save(self.session)
# 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(name=tsigkey['name'])
def delete_tsigkey(self, context, tsigkey):
""" Delete a TSIG Key """
# Delete this TSIG Key itself
tsigkey_m = self._get_tsigkey(tsigkey['id'])
tsigkey_m.delete(self.session)
# Delete this TSIG Key from every domain's metadata
self.session.query(models.DomainMetadata)\
.filter_by(kind='TSIG-ALLOW-AXFR', content=tsigkey['name'])\
.delete()
# Domain Methods
def create_domain(self, context, domain):
servers = self.central_service.get_servers()
servers = self.central_service.get_servers(context)
domain_m = models.Domain()
domain_m.update({
@ -64,6 +129,18 @@ class PowerDNSBackend(base.Backend):
})
record_m.save(self.session)
# Install All TSIG Keys on this domain
tsigkeys = self.session.query(models.TsigKey).all()
for tsigkey in tsigkeys:
domainmetadata_m = models.DomainMetadata()
domainmetadata_m.update({
'domain_id': domain_m.id,
'kind': "TSIG-ALLOW-AXFR",
'content': tsigkey['name']
})
domainmetadata_m.save(self.session)
# NOTE(kiall): Do the SOA last, ensuring we don't trigger a NOTIFY
# before the NS records are in place.
record_m = models.Record()
@ -77,7 +154,7 @@ class PowerDNSBackend(base.Backend):
record_m.save(self.session)
def update_domain(self, context, domain):
servers = self.central_service.get_servers()
servers = self.central_service.get_servers(context)
domain_m = self._get_domain(domain['id'])
@ -95,9 +172,15 @@ class PowerDNSBackend(base.Backend):
domain_m = self._get_domain(domain['id'])
domain_m.delete(self.session)
# Ensure the records are deleted
query = self.session.query(models.Record)
query.filter_by(domain_id=domain_m.id).delete()
# Ensure domainmetadata is deleted
query = self.session.query(models.DomainMetadata)
query.filter_by(domain_id=domain_m.id).delete()
# Record Methods
def create_record(self, context, domain, record):
domain_m = self._get_domain(domain['id'])
record_m = models.Record()
@ -131,6 +214,7 @@ class PowerDNSBackend(base.Backend):
record_m = self._get_record(record['id'])
record_m.delete(self.session)
# Internal Methods
def _sanitize_content(self, type, content):
if type in ('CNAME', 'MX', 'SRV', 'NS', 'PTR'):
return content.rstrip('.')
@ -146,6 +230,18 @@ class PowerDNSBackend(base.Backend):
domain['expire'],
domain['minimum'])
def _get_tsigkey(self, tsigkey_id):
query = self.session.query(models.TsigKey)
try:
tsigkey = query.filter_by(moniker_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)

View File

@ -42,7 +42,7 @@ records = Table('records', meta,
Column('content', String(255), default=None, nullable=True),
Column('ttl', Integer(), default=None, nullable=True),
Column('prio', Integer(), default=None, nullable=True),
Column('change_data', Integer(), default=None,
Column('change_date', Integer(), default=None,
nullable=True),
Column('ordername', String(255), default=None, nullable=True),
Column('auth', Boolean(), default=None, nullable=True))

View File

@ -0,0 +1,53 @@
# Copyright 2012 Managed I.T.
#
# 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 MetaData, Table, Column
from moniker.sqlalchemy.types import UUID
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
tsigkeys_table = Table('tsigkeys', meta, autoload=True)
domains_table = Table('domains', meta, autoload=True)
records_table = Table('records', meta, autoload=True)
tsigkeys_moniker_id = Column('moniker_id', UUID())
tsigkeys_moniker_id.create(tsigkeys_table)
domains_moniker_id = Column('moniker_id', UUID())
domains_moniker_id.create(domains_table)
records_moniker_id = Column('moniker_id', UUID())
records_moniker_id.create(records_table)
def downgrade(migrate_engine):
meta.bind = migrate_engine
tsigkeys_table = Table('tsigkeys', meta, autoload=True)
domains_table = Table('domains', meta, autoload=True)
records_table = Table('records', meta, autoload=True)
tsigkeys_moniker_id = Column('moniker_id', UUID())
tsigkeys_moniker_id.drop(tsigkeys_table)
domains_moniker_id = Column('moniker_id', UUID())
domains_moniker_id.drop(domains_table)
records_moniker_id = Column('moniker_id', UUID())
records_moniker_id.drop(records_table)

View File

@ -28,6 +28,24 @@ class Base(CommonBase):
Base = declarative_base(cls=Base)
class TsigKey(Base):
__tablename__ = 'tsigkeys'
moniker_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'

View File

@ -0,0 +1,40 @@
# Copyright 2012 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.ext.compiler import compiles
from sqlalchemy.sql.expression import Executable, ClauseElement
class InsertFromSelect(Executable, ClauseElement):
execution_options = \
Executable._execution_options.union({'autocommit': True})
def __init__(self, table, select):
self.table = table
self.select = select
@compiles(InsertFromSelect)
def visit_insert_from_select(element, compiler, **kw):
return "INSERT INTO %s %s" % (
compiler.process(element.table, asfrom=True),
compiler.process(element.select)
)
# # Dialect specific compilation example, should it be needed.
# @compiles(InsertFromSelect, 'postgresql')
# def visit_insert_from_select(element, compiler, **kw):
# ...

View File

@ -13,7 +13,6 @@
# 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.exc import IntegrityError
from sqlalchemy.orm import object_mapper
from moniker import exceptions