Add backend for Designate using SECONDARY zones

This will allow the use of a Designate installation as a backend using the
recently added SECONDARY zones feature. The patch includes bits for devstack
and tests.

Implement blueprint: d2d-driver

Change-Id: I5fcaf36482cf692432f7871ef08b2ae7fefe749a
This commit is contained in:
Endre Karlson 2015-08-17 15:47:57 +02:00
parent 746fa57eae
commit e90dbe1ae5
7 changed files with 391 additions and 0 deletions

View File

@ -0,0 +1,136 @@
# lib/designate_plugins/backend-designate
# Configure the designate backend
# Requirements:
# Another Designate service is needed in order to install the SECONDARY zones in it.
# Enable with:
# DESIGNATE_BACKEND_DRIVER=designate
# Dependencies:
# ``functions`` file
# ``designate`` configuration
# install_designate_backend - install any external requirements
# configure_designate_backend - make configuration changes, including those to other services
# init_designate_backend - initialize databases, etc.
# start_designate_backend - start any external services
# stop_designate_backend - stop any external services
# cleanup_designate_backend - remove transient data and cache
# Save trace setting
DP_D2D_XTRACE=$(set +o | grep xtrace)
set +o xtrace
# Defaults
# --------
# This is the Primary Designate MDNS servers.
DESIGNATE_D2D_MASTERS=${DESIGNATE_D2D_MASTERS:-""}
# DNS server to notify (MiniDNS ip:port)
DESIGNATE_D2D_ALSO_NOTIES=${DESIGNATE_D2D_ALSO_NOTIES:-""}
# DNS server to check SOA etc against
DESIGNATE_D2D_NAMESERVERS=${DESIGNATE_D2D_NAMESERVERS:-""}
# Destination openstack credentials
DESIGNATE_D2D_KS_VERSION=${DESIGNATE_D2D_KS_VERSION:-3}
DESIGNATE_D2D_AUTH_URL=${DESIGNATE_D2D_AUTH_URL:-}
DESIGNATE_D2D_USERNAME=${DESIGNATE_D2D_USERNAME:-}
DESIGNATE_D2D_PASSWORD=${DESIGNATE_D2D_PASSWORD:-}
# Keystone V2
DESIGNATE_D2D_TENANT_NAME=${DESIGNATE_D2D_TENANT_NAME:-}
DESIGNATE_D2D_TENANT_NAME=${DESIGNATE_D2D_TENANT_ID:-}
# Keystone V3
DESIGNATE_D2D_PROJECT_NAME=${DESIGNATE_D2D_PROJECT_NAME:-}
DESIGNATE_D2D_PROJECT_DOMAIN_NAME=${DESIGNATE_D2D_PROJECT_DOMAIN_NAME:-}
DESIGNATE_D2D_USER_DOMAIN_NAME=${DESIGNATE_D2D_USER_DOMAIN_NAME:-}
# Entry Points
# ------------
# install_designate_backend - install any external requirements
function install_designate_backend {
:
}
# configure_designate_backend - make configuration changes, including those to other services
function configure_designate_backend {
iniset $DESIGNATE_CONF pool_target:$DESIGNATE_TARGET_ID type designate
iniset $DESIGNATE_CONF pool_target:$DESIGNATE_TARGET_ID masters $DESIGNATE_D2D_MASTERS
options="auth_url: $DESIGNATE_D2D_AUTH_URL, username: $DESIGNATE_D2D_USERNAME, password: $DESIGNATE_D2D_PASSWORD,"
if [ "$DESIGNATE_D2D_KS_VERSION" == "2" ]; then
if [ ! -z "$DESIGNATE_D2D_TENANT_NAME" ]; then
options="$options tenant_name=$DESIGNATE_D2D_TENANT_NAME,"
fi
if [ ! -z "$DESIGNATE_D2D_TENANT_ID" ]; then
options="$options tenant_id=$DESIGNATE_D2D_TENANT_ID,"
fi
fi
if [ ! -z "$DESIGNATE_D2D_KS_VERSION" == "3" ]; then
options="$options project_name: $DESIGNATE_D2D_PROJECT_NAME, project_domain_name=$DESIGNATE_D2D_PROJECT_DOMAIN_NAME, user_domain_name=$DESIGNATE_D2D_USER_DOMAIN_NAME"
fi
iniset $DESIGNATE_CONF pool_target:$DESIGNATE_TARGET_ID options $options
# Create a Pool Nameserver for each of the Designate nameservers
local nameserver_ids=""
IFS=',' read -a nameservers <<< "$DESIGNATE_D2D_NAMESERVERS"
for nameserver in "${nameservers[@]}"; do
local nameserver_id=`uuidgen`
iniset $DESIGNATE_CONF pool_nameserver:$nameserver_id host $(dig +short A $nameserver | head -n 1)
iniset $DESIGNATE_CONF pool_nameserver:$nameserver_id port 53
# Append the Nameserver ID to the list
nameserver_ids+=${nameserver_id},
done
# Configure the Pool for the set of nameserver IDs, minus the trailing comma
iniset $DESIGNATE_CONF pool:$DESIGNATE_POOL_ID nameservers "${nameserver_ids:0:-1}"
# Configure the Pool to Notify the destination Mdns
iniset $DESIGNATE_CONF pool:$DESIGNATE_POOL_ID also_notifies "$DESIGNATE_D2D_ALSO_NOTIFIES"
}
# create_designate_ns_records - Create Pool NS Records
function create_designate_ns_records_backend {
# Build an array of the Designate nameservers.
IFS=',' read -a ns_records <<< "$DESIGNATE_D2D_NAMESERVERS"
# Create a NS Record for each of the Designate nameservers
for ns_record in "${ns_records[@]}"; do
designate server-create --name "${ns_record%%.}."
done
}
# init_designate_backend - initialize databases, etc.
function init_designate_backend {
:
}
# start_designate_backend - start any external services
function start_designate_backend {
:
}
# stop_designate_backend - stop any external services
function stop_designate_backend {
:
}
# cleanup_designate_backend - remove transient data and cache
function cleanup_designate_backend {
:
}
# Restore xtrace
$DP_D2D_XTRACE

View File

@ -56,6 +56,33 @@ ENABLED_SERVICES+=,designate,designate-central,designate-api,designate-pool-mana
#DESIGNATE_AKAMAI_NAMESERVERS=a5-64.akam.net,a11-65.akam.net,a13-66.akam.net,a14-64.akam.net,a20-65.akam.net,a22-66.akam.net
#DESIGNATE_AKAMAI_MASTERS=
# Designate D2D Backend
# NOTEs:
# - DESIGNATE_D2D_ALSO_NOTIFIES needs to be set to the source mdns ip:port in
# order for designate to receive the proper NOTIFY
# - DESIGNATE_D2D_* credentials should be setup either to the source keystone
# or the destination
#DESIGNATE_D2D_MASTERS=
#DESIGNATE_D2D_ALSO_NOTIFIES=
#DESIGNATE_D2D_NAMESERVERS=
# Authentication options
#DESIGNATE_D2D_KS_VERSION=3
#DESIGNATE_D2D_AUTH_URL=
#DESIGNATE_D2D_USERNAME=
#DESIGNATE_D2D_PASSWORD=
# Keystone V2
#DESIGNATE_D2D_TENANT_NAME=${DESIGNATE_D2D_TENANT_NAME:-}
#DESIGNATE_D2D_TENANT_NAME=${DESIGNATE_D2D_TENANT_ID:-}
# Keystone V3
#DESIGNATE_D2D_PROJECT_NAME=
#DESIGNATE_D2D_PROJECT_DOMAIN_NAME=
#DESIGNATE_D2D_USER_DOMAIN_NAME=
# Designate Misc Config
# =====================

View File

@ -0,0 +1,106 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@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 designateclient.v2 import client
from designateclient import exceptions
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import session as ks_session
from oslo_config import cfg
from oslo_log import log as logging
from designate.backend import base
from designate.i18n import _LI
from designate.i18n import _LW
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CFG_GROUP = 'backend:designate'
class DesignateBackend(base.Backend):
"""
Support for Designate to Designate using Secondary zones.
"""
__plugin_name__ = 'designate'
__backend_status__ = 'release-compatible'
def __init__(self, target):
super(DesignateBackend, self).__init__(target)
self.auth_url = self.options.get('auth_url')
self.username = self.options.get('username')
self.password = self.options.get('password')
# ks v2
self.tenant_name = self.options.get('tenant_name')
self.tenant_id = self.options.get('tenant_id')
# ks v3
self.project_name = self.options.get('project_name')
self.project_domain_name = self.options.get(
'project_domain_name', 'default')
self.user_domain_name = self.options.get('user_domain_name', 'default')
self.service_type = self.options.get('service_type', 'dns')
@property
def client(self):
return self._get_client()
def _get_client(self):
if self._client is not None:
return self._client
if (self.tenant_id is not None or self.tenant_name is not None):
auth = v2_auth.Password(
auth_url=self.auth_url,
username=self.username,
password=self.password,
tenant_id=self.tenant_id,
tenant_name=self.tenant_name)
elif self.project_name is not None:
auth = v3_auth.Password(
auth_url=self.auth_url,
username=self.username,
password=self.password,
project_name=self.project_name,
project_domain_name=self.project_domain_name,
user_domain_name=self.user_domain_name)
else:
auth = None
session = ks_session.Session(auth=auth)
self._client = client.Client(
session=session, service_type=self.service_type)
return self._client
def create_domain(self, context, domain):
msg = _LI('Creating domain %(d_id)s / %(d_name)s')
LOG.info(msg, {'d_id': domain['id'], 'd_name': domain['name']})
masters = ["%s:%s" % (i.host, i.port) for i in self.masters]
self.client.zones.create(
domain.name, 'SECONDARY', masters=masters)
def delete_domain(self, context, domain):
msg = _LI('Deleting domain %(d_id)s / %(d_name)s')
LOG.info(msg, {'d_id': domain['id'], 'd_name': domain['name']})
try:
self.client.zones.delete(domain.name)
except exceptions.NotFound:
msg = _LW("Zone %s not found on remote Designate, Ignoring")
LOG.warn(msg, domain.id)

View File

@ -0,0 +1,120 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@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.
import uuid
from designateclient import exceptions
from mock import patch
from mock import NonCallableMagicMock
from mock import Mock
from oslo_log import log as logging
import oslotest.base
import testtools
from designate import objects
from designate.backend import impl_designate
LOG = logging.getLogger(__name__)
def create_zone():
id_ = str(uuid.uuid4())
return objects.Domain(
id=id_,
name='%s-example.com.' % id_,
email='root@example.com',
)
class RoObject(dict):
def __setitem__(self, *a):
raise NotImplementedError
def __setattr__(self, *a):
raise NotImplementedError
def __getattr__(self, k):
return self[k]
class DesignateBackendTest(oslotest.base.BaseTestCase):
def setUp(self):
super(DesignateBackendTest, self).setUp()
opts = RoObject(
username='user',
password='secret',
project_name='project',
project_domain_name='project_domain',
user_domain_name='user_domain'
)
self.target = RoObject({
'id': '4588652b-50e7-46b9-b688-a9bad40a873e',
'type': 'dyndns',
'masters': [RoObject({'host': '192.0.2.1', 'port': 53})],
'options': opts
})
# Backends blow up when trying to self.admin_context = ... due to
# policy not being initialized
self.admin_context = Mock()
get_context_patcher = patch(
'designate.context.DesignateContext.get_admin_context')
get_context = get_context_patcher.start()
get_context.return_value = self.admin_context
self.backend = impl_designate.DesignateBackend(self.target)
# Mock client
self.client = NonCallableMagicMock()
zones = NonCallableMagicMock(spec_set=[
'create', 'delete'])
self.client.configure_mock(zones=zones)
def test_create_domain(self):
zone = create_zone()
masters = ["%(host)s:%(port)s" % self.target.masters[0]]
with patch.object(
self.backend, '_get_client', return_value=self.client):
self.backend.create_domain(self.admin_context, zone)
self.client.zones.create.assert_called_once_with(
zone.name, 'SECONDARY', masters=masters)
def test_delete_domain(self):
zone = create_zone()
with patch.object(
self.backend, '_get_client', return_value=self.client):
self.backend.delete_domain(self.admin_context, zone)
self.client.zones.delete.assert_called_once_with(zone.name)
def test_delete_domain_notfound(self):
zone = create_zone()
self.client.delete.side_effect = exceptions.NotFound
with patch.object(
self.backend, '_get_client', return_value=self.client):
self.backend.delete_domain(self.admin_context, zone)
self.client.zones.delete.assert_called_once_with(zone.name)
def test_delete_domain_exc(self):
class Exc(Exception):
pass
zone = create_zone()
self.client.zones.delete.side_effect = Exc()
with testtools.ExpectedException(Exc):
with patch.object(
self.backend, '_get_client', return_value=self.client):
self.backend.delete_domain(self.admin_context, zone)
self.client.zones.delete.assert_called_once_with(zone.name)

View File

@ -24,6 +24,7 @@ Paste
PasteDeploy>=1.5.0
pbr<2.0,>=1.6
pecan>=1.0.0
python-designateclient>=1.0.0
python-neutronclient>=2.6.0
Routes!=2.0,!=2.1,>=1.12.3;python_version=='2.7'
Routes!=2.0,>=1.12.3;python_version!='2.7'

View File

@ -81,6 +81,7 @@ designate.notification.handler =
designate.backend =
bind9 = designate.backend.impl_bind9:Bind9Backend
designate = designate.backend.impl_designate:DesignateBackend
powerdns = designate.backend.impl_powerdns:PowerDNSBackend
dynect = designate.backend.impl_dynect:DynECTBackend
akamai = designate.backend.impl_akamai:AkamaiBackend