Merge "Implement create/delete zone for Akamai v2 API"
This commit is contained in:
commit
899156da30
|
@ -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'])
|
|
@ -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'
|
||||||
|
)
|
||||||
|
])
|
|
@ -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
|
|
@ -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
|
docutils==0.14
|
||||||
dogpile.cache==0.6.5
|
dogpile.cache==0.6.5
|
||||||
dulwich==0.19.0
|
dulwich==0.19.0
|
||||||
|
edgegrid-python==1.1.1
|
||||||
enum-compat==0.0.2
|
enum-compat==0.0.2
|
||||||
eventlet==0.18.2
|
eventlet==0.18.2
|
||||||
extras==1.0.0
|
extras==1.0.0
|
||||||
|
|
|
@ -49,3 +49,4 @@ debtcollector>=1.2.0 # Apache-2.0
|
||||||
os-win>=3.0.0 # Apache-2.0
|
os-win>=3.0.0 # Apache-2.0
|
||||||
monasca-statsd>=1.1.0 # Apache-2.0
|
monasca-statsd>=1.1.0 # Apache-2.0
|
||||||
futurist>=1.2.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
|
pdns4 = designate.backend.impl_pdns4:PDNS4Backend
|
||||||
dynect = designate.backend.impl_dynect:DynECTBackend
|
dynect = designate.backend.impl_dynect:DynECTBackend
|
||||||
akamai = designate.backend.impl_akamai:AkamaiBackend
|
akamai = designate.backend.impl_akamai:AkamaiBackend
|
||||||
|
akamai_v2 = designate.backend.impl_akamai_v2:AkamaiBackend
|
||||||
nsd4 = designate.backend.impl_nsd4:NSD4Backend
|
nsd4 = designate.backend.impl_nsd4:NSD4Backend
|
||||||
infoblox = designate.backend.impl_infoblox:InfobloxBackend
|
infoblox = designate.backend.impl_infoblox:InfobloxBackend
|
||||||
fake = designate.backend.impl_fake:FakeBackend
|
fake = designate.backend.impl_fake:FakeBackend
|
||||||
|
|
Loading…
Reference in New Issue