PowerDNS Backend Driver

Change-Id: Ia1fad83a0219778ae4704b32b8d8e3a209d046e9
This commit is contained in:
Kiall Mac Innes 2013-01-18 11:47:24 +00:00
parent c52f08ea48
commit 355efb1040
6 changed files with 333 additions and 71 deletions

View File

@ -0,0 +1,210 @@
# 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
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import exc as sqlalchemy_exceptions
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker import exceptions
from moniker.backend import base
from moniker.central import api as central_api
from moniker.context import MonikerContext
from moniker.sqlalchemy.session import get_session, SQLOPTS
from moniker.sqlalchemy.models import Base as CommonBase
from moniker.sqlalchemy.types import UUID
LOG = logging.getLogger(__name__)
cfg.CONF.register_group(cfg.OptGroup(
name='backend:powerdns', title="Configuration for Powerdns Backend"
))
cfg.CONF.register_opts(SQLOPTS, group='backend:powerdns')
class Base(CommonBase):
id = Column(Integer, primary_key=True, autoincrement=True)
Base = declarative_base(cls=Base)
class Domain(Base):
__tablename__ = 'domains'
moniker_id = Column(UUID, nullable=False)
name = Column(String(255), nullable=False, unique=True)
master = Column(String(128), 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'
moniker_id = Column(UUID, nullable=False)
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)
class PowerDNSBackend(base.Backend):
__plugin_name__ = 'powerdns'
def start(self):
super(PowerDNSBackend, self).start()
self.session = get_session(self.name)
def create_domain(self, context, domain):
admin_context = MonikerContext.get_admin_context()
servers = central_api.get_servers(admin_context)
domain_m = Domain()
domain_m.update({
'moniker_id': domain['id'],
'name': domain['name'].rstrip('.'),
'master': servers[0]['name'].rstrip('.'),
'type': 'NATIVE',
'account': context.tenant_id
})
domain_m.save(self.session)
for server in servers:
record_m = Record()
record_m.update({
'moniker_id': server['id'],
'domain_id': domain_m.id,
'name': domain['name'].rstrip('.'),
'type': 'NS',
'content': server['name'].rstrip('.')
})
record_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 = Record()
record_m.update({
'moniker_id': domain['id'],
'domain_id': domain_m.id,
'name': domain['name'].rstrip('.'),
'type': 'SOA',
'content': self._build_soa_content(domain)
})
record_m.save(self.session)
def update_domain(self, context, domain):
domain_m = self._get_domain(domain['id'])
soa_record_m = self._get_record(domain=domain_m, type='SOA')
soa_record_m.update({
'content': self._build_soa_content(domain)
})
soa_record_m.save(self.session)
def delete_domain(self, context, domain):
domain_m = self._get_domain(domain['id'])
domain_m.delete(self.session)
self.session.query(Record).filter_by(domain_id=domain_m.id).delete()
def create_record(self, context, domain, record):
domain_m = self._get_domain(domain['id'])
record_m = Record()
record_m.update({
'moniker_id': record['id'],
'domain_id': domain_m.id,
'name': record['name'].rstrip('.'),
'type': record['type'],
'content': record['data'],
'ttl': record['ttl'],
'prio': record['priority']
})
record_m.save(self.session)
def update_record(self, context, domain, record):
record_m = self._get_record(record['id'])
record_m.update({
'name': record['name'].rstrip('.'),
'type': record['type'],
'content': record['data'],
'ttl': record['ttl'],
'prio': record['priority']
})
record_m.save(self.session)
def delete_record(self, context, domain, record):
record_m = self._get_record(record['id'])
record_m.delete(self.session)
def _build_soa_content(self, domain):
return "%s %s. %d %d %d %d %d" % (domain['name'],
domain['email'].replace("@", "."),
domain['serial'],
domain['refresh'],
domain['retry'],
domain['expire'],
domain['minimum'])
def _get_domain(self, domain_id):
query = self.session.query(Domain)
try:
domain = query.filter_by(moniker_id=domain_id).one()
except sqlalchemy_exceptions.NoResultFound:
raise exceptions.Backend('No domain found')
except sqlalchemy_exceptions.MultipleResultsFound:
raise exceptions.Backend('Too many domains found')
else:
return domain
def _get_record(self, record_id=None, domain=None, type=None):
query = self.session.query(Record)
if record_id:
query = query.filter_by(moniker_id=record_id)
if type:
query = query.filter_by(type=type)
if domain:
query = query.filter_by(domain_id=domain.id)
try:
record = query.one()
except sqlalchemy_exceptions.NoResultFound:
raise exceptions.Backend('No record found')
except sqlalchemy_exceptions.MultipleResultsFound:
raise exceptions.Backend('Too many records found')
else:
return record

View File

@ -19,6 +19,10 @@ class Base(Exception):
pass
class Backend(Base):
pass
class ConfigurationError(Base):
pass

View File

@ -0,0 +1,85 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# Author: Patrick Galbraith <patg@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.exc import IntegrityError
from sqlalchemy.orm import object_mapper
from moniker import exceptions
class Base(object):
__abstract__ = True
__table_initialized__ = False
def save(self, session):
""" Save this object """
session.add(self)
try:
session.flush()
except IntegrityError, e:
non_unique_strings = (
'duplicate entry',
'not unique'
)
for non_unique_string in non_unique_strings:
if non_unique_string in str(e).lower():
raise exceptions.Duplicate(str(e))
# Not a Duplicate error.. Re-raise.
raise
def delete(self, session):
""" Delete this object """
session.delete(self)
session.flush()
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def __iter__(self):
columns = dict(object_mapper(self).columns).keys()
# NOTE(russellb): Allow models to specify other keys that can be looked
# up, beyond the actual db columns. An example would be the 'name'
# property for an Instance.
if hasattr(self, '_extra_keys'):
columns.extend(self._extra_keys())
self._i = iter(columns)
return self
def next(self):
n = self._i.next()
return n, getattr(self, n)
def update(self, values):
""" Make the model object behave like a dict """
for k, v in values.iteritems():
setattr(self, k, v)
def iteritems(self):
"""
Make the model object behave like a dict.
Includes attributes from joins.
"""
local = dict(self)
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
if not k[0] == '_'])
local.update(joined)
return local.iteritems()

View File

@ -17,96 +17,30 @@
# under the License.
from sqlalchemy import (Column, DateTime, String, Text, Integer, ForeignKey,
Enum, Boolean, Unicode)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship, backref, object_mapper
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.hybrid import hybrid_property
from moniker.openstack.common import log as logging
from moniker.openstack.common import timeutils
from moniker.openstack.common.uuidutils import generate_uuid
from moniker import exceptions
from moniker.sqlalchemy.types import UUID, Inet
from moniker.sqlalchemy.models import Base as CommonBase
from sqlalchemy.ext.declarative import declarative_base
LOG = logging.getLogger(__name__)
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'NS', 'PTR']
class Base(object):
__abstract__ = True
__table_initialized__ = False
class Base(CommonBase):
id = Column(UUID, default=generate_uuid, primary_key=True)
version = Column(Integer, default=1, nullable=False)
created_at = Column(DateTime, default=timeutils.utcnow)
updated_at = Column(DateTime, onupdate=timeutils.utcnow)
version = Column(Integer, default=1, nullable=False)
__mapper_args__ = {
'version_id_col': version
}
def save(self, session):
""" Save this object """
session.add(self)
try:
session.flush()
except IntegrityError, e:
non_unique_strings = (
'duplicate entry',
'not unique'
)
for non_unique_string in non_unique_strings:
if non_unique_string in str(e).lower():
raise exceptions.Duplicate(str(e))
# Not a Duplicate error.. Re-raise.
raise
def delete(self, session):
""" Delete this object """
session.delete(self)
session.flush()
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def __iter__(self):
columns = dict(object_mapper(self).columns).keys()
# NOTE(russellb): Allow models to specify other keys that can be looked
# up, beyond the actual db columns. An example would be the 'name'
# property for an Instance.
if hasattr(self, '_extra_keys'):
columns.extend(self._extra_keys())
self._i = iter(columns)
return self
def next(self):
n = self._i.next()
return n, getattr(self, n)
def update(self, values):
""" Make the model object behave like a dict """
for k, v in values.iteritems():
setattr(self, k, v)
def iteritems(self):
"""
Make the model object behave like a dict.
Includes attributes from joins.
"""
local = dict(self)
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
if not k[0] == '_'])
local.update(joined)
return local.iteritems()
Base = declarative_base(cls=Base)

View File

@ -0,0 +1,28 @@
# 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 moniker.openstack.common import log as logging
from moniker.tests.test_backend import BackendTestCase
LOG = logging.getLogger(__name__)
class PowerDNSBackendDriverTestCase(BackendTestCase):
__test__ = True
def setUp(self):
super(PowerDNSBackendDriverTestCase, self).setUp()
self.config(backend_driver='powerdns', group='service:agent')

View File

@ -67,6 +67,7 @@ setup(
[moniker.backend]
bind9 = moniker.backend.impl_bind9:Bind9Backend
mysqlbind9 = moniker.backend.impl_mysqlbind9:MySQLBind9Backend
powerdns = moniker.backend.impl_powerdns:PowerDNSBackend
rpc = moniker.backend.impl_rpc:RPCBackend
fake = moniker.backend.impl_fake:FakeBackend