summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Kraynev <sergejyit@gmail.com>2019-10-03 23:38:10 +0400
committerErik Olof Gunnar Andersson <eandersson@blizzard.com>2020-01-16 18:54:30 +0000
commit318b8d03193d3d9c26842528e9eaf25814df3ba9 (patch)
tree6c7cf1b8d7a385b7ac0299fdd7e47309598a1688
parentf355aae93955b76df9ea9ef5554b451df0ac91b4 (diff)
downloaddesignate-318b8d03193d3d9c26842528e9eaf25814df3ba9.tar.gz
Implement create/delete zone for Akamai v2 API
- Ignore duplicate Zone error - Handle error when contractId or gid is missed - Ignore port for masters servers, because Akamai uses only 53 port and does not allow to specify any port in list of masters servers. - Added timeout and retries for soft Zone Delete - Added handling errors on the delete zone action - Added Log info message with RequestId on soft zone delete - Added processing for TsigKey during creation zone - Added devsatck_plugin for akamai_v2 backend Depends-On: https://review.opendev.org/#/c/692819/4 Change-Id: Ib221f4cf0371e70fc6900582d826ffc1bdfc12b9
-rw-r--r--designate/backend/impl_akamai_v2.py199
-rw-r--r--designate/tests/unit/backend/test_akamai_v2.py494
-rw-r--r--devstack/designate_plugins/backend-akamai-v2161
-rw-r--r--etc/designate/pools.yaml.sample-akamai_v240
-rw-r--r--lower-constraints.txt1
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg1
7 files changed, 897 insertions, 0 deletions
diff --git a/designate/backend/impl_akamai_v2.py b/designate/backend/impl_akamai_v2.py
new file mode 100644
index 00000000..407f81f6
--- /dev/null
+++ b/designate/backend/impl_akamai_v2.py
@@ -0,0 +1,199 @@
+# Copyright 2019 Cloudification GmbH
+#
+# Author: Sergey Kraynev <contact@cloudification.io>
+#
+# 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 time
+
+import requests
+from akamai import edgegrid
+from oslo_log import log as logging
+import six.moves.urllib.parse as urlparse
+
+from designate import exceptions
+from designate.backend import base
+
+
+LOG = logging.getLogger(__name__)
+
+
+class AkamaiClient(object):
+ def __init__(self, client_token=None, client_secret=None,
+ access_token=None, host=None):
+ session = requests.Session()
+ self.baseurl = 'https://%s' % host
+ self.client_token = client_token
+ self.client_secret = client_secret
+ self.access_token = access_token
+
+ session.auth = edgegrid.EdgeGridAuth(
+ client_token=self.client_token,
+ client_secret=self.client_secret,
+ access_token=self.access_token
+ )
+
+ self.http = session
+
+ def gen_url(self, url_path):
+ return urlparse.urljoin(self.baseurl, url_path)
+
+ def post(self, payloads):
+ url_path = payloads.pop('url')
+ return self.http.post(url=self.gen_url(url_path), **payloads)
+
+ def get(self, url_path):
+ return self.http.get(url=self.gen_url(url_path))
+
+ def build_masters_field(self, masters):
+ # Akamai v2 supports only ip and hostnames. Ports could not be
+ # specified explicitly. 53 will be used by default
+ return [master.host for master in masters]
+
+ def gen_tsig_payload(self, target):
+ return {
+ 'name': target.options.get('tsig_key_name'),
+ 'algorithm': target.options.get('tsig_key_algorithm'),
+ 'secret': target.options.get('tsig_key_secret'),
+ }
+
+ def gen_create_payload(self, zone, masters, contract_id, gid, tenant_id,
+ target):
+ if contract_id is None:
+ raise exceptions.Backend(
+ 'contractId is required for zone creation')
+
+ masters = self.build_masters_field(masters)
+ body = {
+ 'zone': zone['name'],
+ 'type': 'secondary',
+ 'comment': 'Created by Designate for Tenant %s' % tenant_id,
+ 'masters': masters,
+ }
+ # Add tsigKey if it exists
+ if target.options.get('tsig_key_name'):
+ # It's not mentioned in doc, but json schema supports specification
+ # TsigKey in the same zone creation body
+ body.update({'tsigKey': self.gen_tsig_payload(target)})
+
+ params = {
+ 'contractId': contract_id,
+ 'gid': gid,
+ }
+ return {
+ 'url': 'config-dns/v2/zones',
+ 'params': params,
+ 'json': body,
+ }
+
+ def create_zone(self, payload):
+ result = self.post(payload)
+ # NOTE: ignore error about duplicate SZ in AKAMAI
+ if result.status_code == 409 and result.reason == 'Conflict':
+ LOG.info("Can't create zone %s because it already exists",
+ payload['json']['zone'])
+
+ elif not result.ok:
+ json_res = result.json()
+ raise exceptions.Backend(
+ 'Zone creation failed due to: %s' % json_res['detail'])
+
+ @staticmethod
+ def gen_delete_payload(zone_name, force):
+ return {
+ 'url': '/config-dns/v2/zones/delete-requests',
+ 'params': {'force': force},
+ 'json': {'zones': [zone_name]},
+ }
+
+ def delete_zone(self, zone_name):
+ # - try to delete with force=True
+ # - if we get Forbidden error - try to delete it with Checks logic
+
+ result = self.post(
+ self.gen_delete_payload(zone_name, force=True))
+
+ if result.status_code == 403 and result.reason == 'Forbidden':
+ result = self.post(
+ self.gen_delete_payload(zone_name, force=False))
+ if result.ok:
+ request_id = result.json().get('requestId')
+ LOG.info('Run soft delete for zone (%s) and requestId (%s)',
+ zone_name, request_id)
+
+ if request_id is None:
+ reason = 'requestId missed in response'
+ raise exceptions.Backend(
+ 'Zone deletion failed due to: %s' % reason)
+
+ self.validate_deletion_is_complete(request_id)
+
+ if not result.ok and result.status_code != 404:
+ reason = result.json().get('detail') or result.json()
+ raise exceptions.Backend(
+ 'Zone deletion failed due to: %s' % reason)
+
+ def validate_deletion_is_complete(self, request_id):
+ check_url = '/config-dns/v2/zones/delete-requests/%s' % request_id
+ deleted = False
+ attempt = 0
+ while not deleted and attempt < 10:
+ result = self.get(check_url)
+ deleted = result.json()['isComplete']
+ attempt += 1
+ time.sleep(1.0)
+
+ if not deleted:
+ raise exceptions.Backend(
+ 'Zone was not deleted after %s attempts' % attempt)
+
+
+class AkamaiBackend(base.Backend):
+ __plugin_name__ = 'akamai_v2'
+
+ __backend_status__ = 'untested'
+
+ def __init__(self, target):
+ super(AkamaiBackend, self).__init__(target)
+
+ self._host = self.options.get('host', '127.0.0.1')
+ self._port = int(self.options.get('port', 53))
+ self.client = self.init_client()
+
+ def init_client(self):
+ baseurl = self.options.get('akamai_host', '127.0.0.1')
+ client_token = self.options.get('akamai_client_token', 'admin')
+ client_secret = self.options.get('akamai_client_secret', 'admin')
+ access_token = self.options.get('akamai_access_token', 'admin')
+
+ return AkamaiClient(client_token, client_secret, access_token, baseurl)
+
+ def create_zone(self, context, zone):
+ """Create a DNS zone"""
+ LOG.debug('Create Zone')
+ contract_id = self.options.get('akamai_contract_id')
+ gid = self.options.get('akamai_gid')
+ project_id = context.project_id or zone.tenant_id
+ # Take list of masters from pools.yaml
+ payload = self.client.gen_create_payload(
+ zone, self.masters, contract_id, gid, project_id, self.target)
+ self.client.create_zone(payload)
+
+ self.mdns_api.notify_zone_changed(
+ context, zone, self._host, self._port, self.timeout,
+ self.retry_interval, self.max_retries, self.delay)
+
+ def delete_zone(self, context, zone):
+ """Delete a DNS zone"""
+ LOG.debug('Delete Zone')
+ self.client.delete_zone(zone['name'])
diff --git a/designate/tests/unit/backend/test_akamai_v2.py b/designate/tests/unit/backend/test_akamai_v2.py
new file mode 100644
index 00000000..f3dcfa35
--- /dev/null
+++ b/designate/tests/unit/backend/test_akamai_v2.py
@@ -0,0 +1,494 @@
+# Copyright 2019 Cloudification GmbH
+#
+# Author: Sergey Kraynev <contact@cloudification.io>
+#
+# 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 json
+import mock
+import requests
+
+import designate.tests
+from designate import exceptions
+from designate import objects
+from designate.backend import impl_akamai_v2 as akamai
+from designate.tests import fixtures
+
+
+class AkamaiBackendTestCase(designate.tests.TestCase):
+ def setUp(self):
+ super(AkamaiBackendTestCase, self).setUp()
+ self.zone = objects.Zone(
+ id='cca7908b-dad4-4c50-adba-fb67d4c556e8',
+ name='example.com.',
+ email='example@example.com'
+ )
+
+ self.target = {
+ 'id': '4588652b-50e7-46b9-b688-a9bad40a873e',
+ 'type': 'akamai_v2',
+ 'masters': [
+ {'host': '192.168.1.1', 'port': 53},
+ {'host': '192.168.1.2', 'port': 35}
+ ],
+ 'options': [
+ {'key': 'host', 'value': '192.168.2.3'},
+ {'key': 'port', 'value': '53'},
+ {'key': 'akamai_client_secret', 'value': 'client_secret'},
+ {'key': 'akamai_host', 'value': 'host_value'},
+ {'key': 'akamai_access_token', 'value': 'access_token'},
+ {'key': 'akamai_client_token', 'value': 'client_token'},
+ {'key': 'akamai_contract_id', 'value': 'G-XYW'},
+ {'key': 'akamai_gid', 'value': '777'}
+ ],
+ }
+
+ def gen_response(self, status_code, reason, json_data=None):
+ response = requests.models.Response()
+ response.status_code = status_code
+ response.reason = reason
+ response._content = json.dumps(json_data or {}).encode('utf-8')
+ return response
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_create_zone_missed_contract_id(self, mock_post, mock_auth):
+ self.target['options'].remove(
+ {'key': 'akamai_contract_id', 'value': 'G-XYW'})
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ with fixtures.random_seed(0):
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'contractId is required for zone creation',
+ backend.create_zone, self.admin_context, self.zone)
+
+ mock_post.assert_not_called()
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_create_zone(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ with fixtures.random_seed(0):
+ backend.create_zone(self.admin_context, self.zone)
+
+ project_id = self.admin_context.project_id or self.zone.tenant_id
+ mock_post.assert_called_once_with(
+ json={
+ 'comment': 'Created by Designate for Tenant %s' % project_id,
+ 'masters': ['192.168.1.1', '192.168.1.2'],
+ 'type': 'secondary', 'zone': u'example.com.'
+ },
+ params={
+ 'gid': '777',
+ 'contractId': 'G-XYW'
+ },
+ url='https://host_value/config-dns/v2/zones'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_create_zone_duplicate_zone(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.return_value = self.gen_response(409, 'Conflict')
+
+ with fixtures.random_seed(0):
+ backend.create_zone(self.admin_context, self.zone)
+
+ project_id = self.admin_context.project_id or self.zone.tenant_id
+ mock_post.assert_called_once_with(
+ json={
+ 'comment': 'Created by Designate for Tenant %s' % project_id,
+ 'masters': ['192.168.1.1', '192.168.1.2'],
+ 'type': 'secondary', 'zone': u'example.com.'
+ },
+ params={
+ 'gid': '777',
+ 'contractId': 'G-XYW'
+ },
+ url='https://host_value/config-dns/v2/zones'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_create_zone_with_tsig_key(self, mock_post, mock_auth):
+ self.target['options'].extend([
+ {'key': 'tsig_key_name', 'value': 'test_key'},
+ {'key': 'tsig_key_algorithm', 'value': 'hmac-sha512'},
+ {'key': 'tsig_key_secret', 'value': 'aaaabbbbccc'}
+ ])
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ with fixtures.random_seed(0):
+ backend.create_zone(self.admin_context, self.zone)
+
+ project_id = self.admin_context.project_id or self.zone.tenant_id
+ mock_post.assert_called_once_with(
+ json={
+ 'comment': 'Created by Designate for Tenant %s' % project_id,
+ 'masters': ['192.168.1.1', '192.168.1.2'],
+ 'type': 'secondary',
+ 'zone': 'example.com.',
+ 'tsigKey': {
+ 'name': 'test_key',
+ 'algorithm': 'hmac-sha512',
+ 'secret': 'aaaabbbbccc',
+ }
+ },
+ params={
+ 'gid': '777',
+ 'contractId': 'G-XYW'
+ },
+ url='https://host_value/config-dns/v2/zones'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_create_zone_raise_error(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ json_data = {
+ 'title': 'Missing parameter',
+ 'detail': 'Missed A option'
+ }
+ mock_post.return_value = self.gen_response(
+ 400, 'Bad Request', json_data)
+
+ with fixtures.random_seed(0):
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'Zone creation failed due to: Missed A option',
+ backend.create_zone, self.admin_context, self.zone)
+
+ project_id = self.admin_context.project_id or self.zone.tenant_id
+ mock_post.assert_called_once_with(
+ json={
+ 'comment': 'Created by Designate for Tenant %s' % project_id,
+ 'masters': ['192.168.1.1', '192.168.1.2'],
+ 'type': 'secondary', 'zone': 'example.com.'
+ },
+ params={
+ 'gid': '777',
+ 'contractId': 'G-XYW'
+ },
+ url='https://host_value/config-dns/v2/zones'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_force_delete_zone(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.return_value = self.gen_response(200, 'Success')
+
+ with fixtures.random_seed(0):
+ backend.delete_zone(self.admin_context, self.zone)
+
+ mock_post.assert_called_once_with(
+ json={
+ 'zones': ['example.com.']
+ },
+ params={
+ 'force': True
+ },
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_force_delete_zone_raise_error(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.return_value = self.gen_response(
+ 403, 'Bad Request', {'detail': 'Unexpected error'})
+
+ with fixtures.random_seed(0):
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'Zone deletion failed due to: Unexpected error',
+ backend.delete_zone, self.admin_context, self.zone)
+
+ mock_post.assert_called_once_with(
+ json={
+ 'zones': ['example.com.']
+ },
+ params={
+ 'force': True
+ },
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_force_delete_zone_raise_error_404(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.return_value = self.gen_response(
+ 404, 'Bad Request', {'detail': 'Unexpected error'})
+
+ with fixtures.random_seed(0):
+ backend.delete_zone(self.admin_context, self.zone)
+
+ mock_post.assert_called_once_with(
+ json={
+ 'zones': ['example.com.']
+ },
+ params={
+ 'force': True
+ },
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ @mock.patch.object(akamai.requests.Session, 'get')
+ def test_soft_delete_zone(self, mock_get, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.side_effect = [
+ # emulate, when Force=True is forbidden
+ self.gen_response(403, 'Forbidden'),
+ # emulate request, when Force=False
+ self.gen_response(200, 'Success', {'requestId': 'nice_id'}),
+ ]
+
+ # emulate max 9 failed attempts and 1 success
+ mock_get.side_effect = 9 * [
+ self.gen_response(200, 'Success', {'isComplete': False})
+ ] + [
+ self.gen_response(200, 'Success', {'isComplete': True})
+ ]
+
+ with fixtures.random_seed(0), \
+ mock.patch.object(akamai.time, 'sleep') as mock_sleep:
+ mock_sleep.return_value = None
+ backend.delete_zone(self.admin_context, self.zone)
+
+ self.assertEqual(10, mock_sleep.call_count)
+
+ url = 'https://host_value/config-dns/v2/zones/delete-requests/nice_id'
+ mock_get.assert_has_calls(9 * [mock.call(url=url)])
+
+ mock_post.assert_has_calls([
+ mock.call(
+ json={'zones': ['example.com.']},
+ params={'force': True},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ ),
+ mock.call(
+ json={'zones': ['example.com.']},
+ params={'force': False},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+ ])
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ @mock.patch.object(akamai.requests.Session, 'get')
+ def test_soft_delete_zone_failed_after_10_attempts(
+ self, mock_get, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.side_effect = [
+ # emulate, when Force=True is forbidden
+ self.gen_response(403, 'Forbidden'),
+ # emulate request, when Force=False
+ self.gen_response(200, 'Success', {'requestId': 'nice_id'}),
+ ]
+
+ # emulate max 10 failed attempts
+ mock_get.side_effect = 10 * [
+ self.gen_response(200, 'Success', {'isComplete': False})
+ ]
+
+ with fixtures.random_seed(0), \
+ mock.patch.object(akamai.time, 'sleep') as mock_sleep:
+ mock_sleep.return_value = None
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'Zone was not deleted after 10 attempts',
+ backend.delete_zone, self.admin_context, self.zone)
+
+ self.assertEqual(10, mock_sleep.call_count)
+
+ url = 'https://host_value/config-dns/v2/zones/delete-requests/nice_id'
+ mock_get.assert_has_calls(10 * [mock.call(url=url)])
+
+ mock_post.assert_has_calls([
+ mock.call(
+ json={'zones': ['example.com.']},
+ params={'force': True},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ ),
+ mock.call(
+ json={'zones': ['example.com.']},
+ params={'force': False},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+ ])
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_soft_delete_zone_raise_error(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.side_effect = [
+ # emulate, when Force=True is forbidden
+ self.gen_response(403, 'Forbidden'),
+ # emulate request, when Force=False
+ self.gen_response(409, 'Conflict', {'detail': 'Intenal Error'})
+ ]
+
+ with fixtures.random_seed(0):
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'Zone deletion failed due to: Intenal Error',
+ backend.delete_zone, self.admin_context, self.zone)
+
+ mock_post.assert_has_calls([
+ mock.call(
+ json={'zones': [u'example.com.']},
+ params={'force': True},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ ),
+ mock.call(
+ json={'zones': [u'example.com.']},
+ params={'force': False},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+ ])
+
+ @mock.patch.object(akamai, 'edgegrid')
+ @mock.patch.object(akamai.requests.Session, 'post')
+ def test_soft_delete_zone_missed_request_id(self, mock_post, mock_auth):
+ backend = akamai.AkamaiBackend(
+ objects.PoolTarget.from_dict(self.target)
+ )
+ mock_auth.EdgeGridAuth.assert_called_once_with(
+
+ access_token='access_token',
+ client_secret='client_secret',
+ client_token='client_token'
+ )
+
+ mock_post.side_effect = [
+ # emulate, when Force=True is forbidden
+ self.gen_response(403, 'Forbidden'),
+ # emulate request, when Force=False
+ self.gen_response(200, 'Success')
+ ]
+
+ with fixtures.random_seed(0):
+ self.assertRaisesRegex(
+ exceptions.Backend,
+ 'Zone deletion failed due to: requestId missed in response',
+ backend.delete_zone, self.admin_context, self.zone)
+
+ mock_post.assert_has_calls([
+ mock.call(
+ json={'zones': [u'example.com.']},
+ params={'force': True},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ ),
+ mock.call(
+ json={'zones': [u'example.com.']},
+ params={'force': False},
+ url='https://host_value/config-dns/v2/zones/delete-requests'
+ )
+ ])
diff --git a/devstack/designate_plugins/backend-akamai-v2 b/devstack/designate_plugins/backend-akamai-v2
new file mode 100644
index 00000000..ce6a2198
--- /dev/null
+++ b/devstack/designate_plugins/backend-akamai-v2
@@ -0,0 +1,161 @@
+# Configure the Akamai v2 backend
+
+# Requirements:
+# An active Akamai account / contract will be requied to use this DevStack
+# plugin.
+
+# Enable with:
+# DESIGNATE_BACKEND_DRIVER=akamai_v2
+
+# 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_AKAMAI_XTRACE=$(set +o | grep xtrace)
+set +o xtrace
+
+# Defaults
+# --------
+
+# DESIGNATE_HOST is IP address of the one of AKAMAI_NAMESERVERS
+DESIGNATE_HOST=${DESIGNATE_HOST:-"193.108.91.197"}
+DESIGNATE_AKAMAI_CLIENT_SECRET=${DESIGNATE_AKAMAI_CLIENT_SECRET:-"client_secret_string"}
+DESIGNATE_AKAMAI_HOST=${DESIGNATE_AKAMAI_HOST:-"akamai_host_string"}
+DESIGNATE_AKAMAI_ACCESS_TOKEN=${DESIGNATE_AKAMAI_ACCESS_TOKEN:-"access_token_string"}
+DESIGNATE_AKAMAI_CLIENT_TOKEN=${DESIGNATE_AKAMAI_CLIENT_TOKEN:-"client_token_string"}
+DESIGNATE_AKAMAI_CONTRACT_ID=${DESIGNATE_AKAMAI_CONTRACT_ID:-"contract_id"}
+DESIGNATE_AKAMAI_GID=${DESIGNATE_AKAMAI_GID:-"group_id"}
+DESIGNATE_AKAMAI_MASTERS=${DESIGNATE_AKAMAI_MASTERS:-"$DESIGNATE_SERVICE_HOST:$DESIGNATE_SERVICE_PORT_MDNS"}
+DESIGNATE_AKAMAI_NAMESERVERS=${DESIGNATE_AKAMAI_NAMESERVERS:-""}
+DESIGNATE_AKAMAI_ALSO_NOTIFIES=${DESIGNATE_AKAMAI_ALSO_NOTIFIES:-"23.14.128.185,23.207.197.166,23.205.121.134,104.122.95.88,72.247.124.98"}
+
+# Sanity Checks
+# -------------
+if [ -z "$DESIGNATE_AKAMAI_NAMESERVERS" ]; then
+ die $LINENO "You must configure DESIGNATE_AKAMAI_NAMESERVERS"
+fi
+
+if [ "$DESIGNATE_SERVICE_PORT_MDNS" != "53" ]; then
+ die $LINENO "Akamai requires DESIGNATE_SERVICE_PORT_MDNS is set to '53'"
+fi
+
+# 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 {
+ # Generate Designate pool.yaml file
+ sudo tee $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+---
+- name: default
+ description: DevStack Akamai Pool
+ attributes: {}
+
+ targets:
+ - type: akamai
+ description: Akamai API
+ options:
+ host: $DESIGNATE_HOST
+ port: 53
+ akamai_client_secret: $DESIGNATE_AKAMAI_CLIENT_SECRET
+ akamai_host: $DESIGNATE_AKAMAI_HOST
+ akamai_access_token: $DESIGNATE_AKAMAI_ACCESS_TOKEN
+ akamai_client_token: $DESIGNATE_AKAMAI_CLIENT_TOKEN
+ akamai_contract_id: $DESIGNATE_AKAMAI_CONTRACT_ID
+ akamai_gid: $DESIGNATE_AKAMAI_GID
+
+ # NOTE: TSIG key has to be set manully if it's necessary
+ #tsig_key_name: key_test
+ #tsig_key_algorithm: hmac-sha512
+ #tsig_key_secret: test_ley_secret
+
+
+ masters:
+EOF
+
+ # Create a Pool Master for each of the Akamai Masters
+ IFS=',' read -a masters <<< "$DESIGNATE_AKAMAI_MASTERS"
+
+ for master in "${masters[@]}"; do
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ - host: $master
+ port: 53
+EOF
+ done
+
+ # Create a Pool NS Record for each of the Akamai Nameservers
+ IFS=',' read -a nameservers <<< "$DESIGNATE_AKAMAI_NAMESERVERS"
+
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ ns_records:
+EOF
+
+ for nameserver in "${nameservers[@]}"; do
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ - hostname: $nameserver
+ priority: 1
+EOF
+ done
+
+ # Create a Pool Nameserver for each of the Akamai Nameservers
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ nameservers:
+EOF
+
+ for nameserver in "${nameservers[@]}"; do
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ - host: `dig +short A $nameserver | head -n 1`
+ port: 53
+EOF
+ done
+
+ # Create a Pool Also Notifies for each of the Akamai Also Notifies
+ IFS=',' read -a also_notifies <<< "$DESIGNATE_AKAMAI_ALSO_NOTIFIES"
+
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ also_notifies:
+EOF
+
+ for also_notify in "${also_notifies[@]}"; do
+ sudo tee -a $DESIGNATE_CONF_DIR/pools.yaml > /dev/null <<EOF
+ - host: $also_notify
+ port: 53
+EOF
+ 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_AKAMAI_XTRACE
diff --git a/etc/designate/pools.yaml.sample-akamai_v2 b/etc/designate/pools.yaml.sample-akamai_v2
new file mode 100644
index 00000000..ebdcc8f8
--- /dev/null
+++ b/etc/designate/pools.yaml.sample-akamai_v2
@@ -0,0 +1,40 @@
+- name: default-akamai-v2
+ # The name is immutable. There will be no option to change the name after
+ # creation and the only way will to change it will be to delete it
+ # (and all zones associated with it) and recreate it.
+ description: Akamai v2
+
+ attributes: {}
+
+ # List out the NS records for zones hosted within this pool
+ ns_records:
+ - hostname: ns1-1.example.org.
+ priority: 1
+
+ # List out the nameservers for this pool. These are the actual Akamai servers.
+ # We use these to verify changes have propagated to all nameservers.
+ nameservers:
+ - host: 192.0.2.2
+ port: 53
+
+ # List out the targets for this pool. For Akamai, most often, there will be
+ # one entry for each Akamai server.
+ targets:
+ - type: akamai_v2
+ description: Akamai v2 server
+
+ # List out the designate-mdns servers from which Akamai servers should
+ # request zone transfers (AXFRs) from.
+ masters:
+ - host: 192.0.2.1
+ port: 5354
+
+ options:
+ host: 192.0.2.2
+ port: 53
+ akamai_host: 192.0.2.2
+ akamai_client_token: client_token_string
+ akamai_access_token: access_token_string
+ akamai_client_secret: client_secret_string
+ akamai_contract_id: contract_id
+ akamai_gid: group_id
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 5952f01c..b30ab6f3 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -27,6 +27,7 @@ doc8==0.6.0
docutils==0.14
dogpile.cache==0.6.5
dulwich==0.19.0
+edgegrid-python==1.1.1
enum-compat==0.0.2
eventlet==0.18.2
extras==1.0.0
diff --git a/requirements.txt b/requirements.txt
index 9445351f..ccdae9f7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -49,3 +49,4 @@ debtcollector>=1.2.0 # Apache-2.0
os-win>=3.0.0 # Apache-2.0
monasca-statsd>=1.1.0 # Apache-2.0
futurist>=1.2.0 # Apache-2.0
+edgegrid-python>=1.1.1 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 3495e4ab..94519a3e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -75,6 +75,7 @@ designate.backend =
pdns4 = designate.backend.impl_pdns4:PDNS4Backend
dynect = designate.backend.impl_dynect:DynECTBackend
akamai = designate.backend.impl_akamai:AkamaiBackend
+ akamai_v2 = designate.backend.impl_akamai_v2:AkamaiBackend
nsd4 = designate.backend.impl_nsd4:NSD4Backend
infoblox = designate.backend.impl_infoblox:InfobloxBackend
fake = designate.backend.impl_fake:FakeBackend