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
This commit is contained in:
parent
f355aae939
commit
318b8d0319
199
designate/backend/impl_akamai_v2.py
Normal file
199
designate/backend/impl_akamai_v2.py
Normal file
@ -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'])
|
494
designate/tests/unit/backend/test_akamai_v2.py
Normal file
494
designate/tests/unit/backend/test_akamai_v2.py
Normal file
@ -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'
|
||||
)
|
||||
])
|
161
devstack/designate_plugins/backend-akamai-v2
Normal file
161
devstack/designate_plugins/backend-akamai-v2
Normal file
@ -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
|
40
etc/designate/pools.yaml.sample-akamai_v2
Normal file
40
etc/designate/pools.yaml.sample-akamai_v2
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user