diff --git a/contrib/devstack/lib/designate_plugins/backend-designate b/contrib/devstack/lib/designate_plugins/backend-designate new file mode 100644 index 000000000..e911d99b6 --- /dev/null +++ b/contrib/devstack/lib/designate_plugins/backend-designate @@ -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 diff --git a/contrib/vagrant/localrc b/contrib/vagrant/localrc index c61cce554..a3d0bd00b 100644 --- a/contrib/vagrant/localrc +++ b/contrib/vagrant/localrc @@ -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 # ===================== diff --git a/designate/backend/impl_designate.py b/designate/backend/impl_designate.py new file mode 100644 index 000000000..0ca0f1dcf --- /dev/null +++ b/designate/backend/impl_designate.py @@ -0,0 +1,106 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# 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) diff --git a/designate/tests/unit/test_backend/__init__.py b/designate/tests/unit/test_backend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/designate/tests/unit/test_backend/test_designate.py b/designate/tests/unit/test_backend/test_designate.py new file mode 100644 index 000000000..001ef9384 --- /dev/null +++ b/designate/tests/unit/test_backend/test_designate.py @@ -0,0 +1,120 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# 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) diff --git a/requirements.txt b/requirements.txt index ea79b5cf6..4be268fef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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' diff --git a/setup.cfg b/setup.cfg index 37498767c..1f81e6c76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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