diff options
36 files changed, 1071 insertions, 301 deletions
@@ -15,7 +15,7 @@ s-object: false s-proxy: false c-bak: false - tox_envlist: all-plugin + tox_envlist: all tempest_test_regex: | designate_tempest_plugin.* required-projects: &base_required_projects diff --git a/api-ref/source/conf.py b/api-ref/source/conf.py index 617d4ece..1cf3b6c5 100644 --- a/api-ref/source/conf.py +++ b/api-ref/source/conf.py @@ -23,15 +23,9 @@ # serve to show the default. import os -import subprocess import sys -from designate.version import version_info - -import openstackdocstheme - html_theme = 'openstackdocs' -html_theme_path = [openstackdocstheme.get_html_theme_path()] html_theme_options = { "sidebar_mode": "toc", "sidebar_dropdown": "api_ref", @@ -39,6 +33,7 @@ html_theme_options = { extensions = [ 'os_api_ref', + 'openstackdocstheme' ] # If extensions (or modules to document with autodoc) are in another directory, @@ -71,14 +66,6 @@ copyright = u'OpenStack Foundation' # |version| and |release|, also used in various other places throughout the # built documents. # -# The full version, including alpha/beta/rc tags. -release = version_info.release_string() -# The short X.Y version. -version = version_info.version_string() - -# html_context allows us to pass arbitrary values into the html template -html_context = {'bug_tag': 'api-ref', - 'bug_project': 'designate'} # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -151,13 +138,6 @@ pygments_style = 'sphinx' # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' -git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", - "-n1"] -html_last_updated_fmt = subprocess.check_output(git_cmd).decode('utf-8') - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True diff --git a/designate/api/wsgi.py b/designate/api/wsgi.py index 46b80593..0103c9aa 100644 --- a/designate/api/wsgi.py +++ b/designate/api/wsgi.py @@ -15,7 +15,6 @@ import os from oslo_config import cfg from oslo_log import log as logging -from oslo_service import threadgroup from paste import deploy from designate import conf @@ -48,9 +47,7 @@ def init_application(): if not rpc.initialized(): rpc.init(CONF) - heartbeat = service.Heartbeat( - 'api', threadgroup.ThreadGroup(thread_pool_size=1) - ) + heartbeat = service.Heartbeat('api') heartbeat.start() conf = conf_files[0] 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/cmd/agent.py b/designate/cmd/agent.py index e96a5d59..edc7f46d 100644 --- a/designate/cmd/agent.py +++ b/designate/cmd/agent.py @@ -38,7 +38,7 @@ def main(): hookpoints.log_hook_setup() server = agent_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:agent'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/api.py b/designate/cmd/api.py index 6ac6d558..61b07911 100644 --- a/designate/cmd/api.py +++ b/designate/cmd/api.py @@ -40,7 +40,7 @@ def main(): hookpoints.log_hook_setup() server = api_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:api'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/central.py b/designate/cmd/central.py index 80ee8b93..6ae0524a 100644 --- a/designate/cmd/central.py +++ b/designate/cmd/central.py @@ -38,8 +38,7 @@ def main(): hookpoints.log_hook_setup() server = central_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg, - rpc_api=server) + heartbeat = service.Heartbeat(server.service_name, rpc_api=server) service.serve(server, workers=CONF['service:central'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/mdns.py b/designate/cmd/mdns.py index e79586dc..b8993d9c 100644 --- a/designate/cmd/mdns.py +++ b/designate/cmd/mdns.py @@ -38,7 +38,7 @@ def main(): hookpoints.log_hook_setup() server = mdns_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:mdns'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/producer.py b/designate/cmd/producer.py index 4bf18732..89808f27 100644 --- a/designate/cmd/producer.py +++ b/designate/cmd/producer.py @@ -38,7 +38,7 @@ def main(): hookpoints.log_hook_setup() server = producer_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:producer'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/sink.py b/designate/cmd/sink.py index 7242b198..30725712 100644 --- a/designate/cmd/sink.py +++ b/designate/cmd/sink.py @@ -38,7 +38,7 @@ def main(): hookpoints.log_hook_setup() server = sink_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:sink'].workers) heartbeat.start() service.wait() diff --git a/designate/cmd/worker.py b/designate/cmd/worker.py index e0046ac2..2e0f07af 100644 --- a/designate/cmd/worker.py +++ b/designate/cmd/worker.py @@ -38,7 +38,7 @@ def main(): hookpoints.log_hook_setup() server = worker_service.Service() - heartbeat = service.Heartbeat(server.service_name, server.tg) + heartbeat = service.Heartbeat(server.service_name) service.serve(server, workers=CONF['service:worker'].workers) heartbeat.start() service.wait() diff --git a/designate/service.py b/designate/service.py index 3ad8e9b3..22f92532 100644 --- a/designate/service.py +++ b/designate/service.py @@ -69,9 +69,8 @@ class Service(service.Service): class Heartbeat(object): - def __init__(self, name, tg, rpc_api=None): + def __init__(self, name, rpc_api=None): self.name = name - self.tg = tg self._status = 'UP' self._stats = {} @@ -81,7 +80,7 @@ class Heartbeat(object): CONF.heartbeat_emitter.emitter_type ) self.heartbeat_emitter = emitter_cls( - self.name, self.tg, + self.name, status_factory=self.get_status, rpc_api=rpc_api ) diff --git a/designate/service_status.py b/designate/service_status.py index b6f0ba24..22d4410e 100644 --- a/designate/service_status.py +++ b/designate/service_status.py @@ -14,6 +14,7 @@ import abc from oslo_log import log as logging +from oslo_service import loopingcall from oslo_utils import timeutils import designate.conf @@ -31,19 +32,15 @@ class HeartBeatEmitter(plugin.DriverPlugin): __plugin_ns__ = 'designate.heartbeat_emitter' __plugin_type__ = 'heartbeat_emitter' - def __init__(self, service, thread_group, status_factory=None, - *args, **kwargs): + def __init__(self, service, status_factory=None, *args, **kwargs): super(HeartBeatEmitter, self).__init__() self._service = service self._hostname = CONF.host - self._running = False - self._tg = thread_group - self._tg.add_timer( - CONF.heartbeat_emitter.heartbeat_interval, - self._emit_heartbeat) - + self._timer = loopingcall.FixedIntervalLoopingCall( + self._emit_heartbeat + ) self._status_factory = status_factory def _get_status(self): @@ -56,9 +53,6 @@ class HeartBeatEmitter(plugin.DriverPlugin): """ Returns Status, Stats, Capabilities """ - if not self._running: - return - status, stats, capabilities = self._get_status() service_status = objects.ServiceStatus( @@ -79,10 +73,13 @@ class HeartBeatEmitter(plugin.DriverPlugin): pass def start(self): - self._running = True + self._timer.start( + CONF.heartbeat_emitter.heartbeat_interval, + stop_on_exception=False + ) def stop(self): - self._running = False + self._timer.stop() class NoopEmitter(HeartBeatEmitter): @@ -95,9 +92,8 @@ class NoopEmitter(HeartBeatEmitter): class RpcEmitter(HeartBeatEmitter): __plugin_name__ = 'rpc' - def __init__(self, service, thread_group, rpc_api=None, *args, **kwargs): - super(RpcEmitter, self).__init__( - service, thread_group, *args, **kwargs) + def __init__(self, service, rpc_api=None, *args, **kwargs): + super(RpcEmitter, self).__init__(service, *args, **kwargs) self.rpc_api = rpc_api def _transmit(self, status): 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/designate/tests/unit/test_heartbeat.py b/designate/tests/unit/test_heartbeat.py index fbb01862..a531d5b0 100644 --- a/designate/tests/unit/test_heartbeat.py +++ b/designate/tests/unit/test_heartbeat.py @@ -13,6 +13,7 @@ import mock import oslotest.base from oslo_config import cfg from oslo_config import fixture as cfg_fixture +from oslo_service import loopingcall from designate import service @@ -20,14 +21,18 @@ CONF = cfg.CONF class HeartbeatTest(oslotest.base.BaseTestCase): - def setUp(self): + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def setUp(self, mock_looping): super(HeartbeatTest, self).setUp() + + self.mock_timer = mock.Mock() + mock_looping.return_value = self.mock_timer + self.useFixture(cfg_fixture.Config(CONF)) CONF.set_override('emitter_type', 'noop', 'heartbeat_emitter') - self.mock_tg = mock.Mock() - self.heartbeat = service.Heartbeat('test', self.mock_tg) + self.heartbeat = service.Heartbeat('test') def test_get_status(self): self.assertEqual(('UP', {}, {},), self.heartbeat.get_status()) @@ -38,16 +43,13 @@ class HeartbeatTest(oslotest.base.BaseTestCase): ) def test_start_heartbeat(self): - self.assertFalse(self.heartbeat.heartbeat_emitter._running) - self.heartbeat.start() - self.assertTrue(self.heartbeat.heartbeat_emitter._running) + self.mock_timer.start.assert_called_once() def test_stop_heartbeat(self): - self.assertFalse(self.heartbeat.heartbeat_emitter._running) self.heartbeat.start() self.heartbeat.stop() - self.assertFalse(self.heartbeat.heartbeat_emitter._running) + self.mock_timer.stop.assert_called_once() diff --git a/designate/tests/unit/test_service_status.py b/designate/tests/unit/test_service_status.py index a8fe3880..c7827289 100644 --- a/designate/tests/unit/test_service_status.py +++ b/designate/tests/unit/test_service_status.py @@ -14,6 +14,7 @@ import mock import oslotest.base from oslo_config import cfg +from oslo_service import loopingcall from designate import objects from designate import service_status @@ -22,40 +23,46 @@ from designate import service_status class NoopEmitterTest(oslotest.base.BaseTestCase): def setUp(self): super(NoopEmitterTest, self).setUp() - self.mock_tg = mock.Mock() - def test_init(self): - service_status.NoopEmitter("svc", self.mock_tg) + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def test_start(self, mock_looping): + mock_timer = mock.Mock() + mock_looping.return_value = mock_timer - def test_start(self): - emitter = service_status.NoopEmitter("svc", self.mock_tg) + emitter = service_status.NoopEmitter("svc") emitter.start() - self.mock_tg.add_timer.assert_called_once_with( - 10.0, emitter._emit_heartbeat) + mock_timer.start.assert_called_once() - def test_stop(self): - mock_pulse = mock.Mock() - self.mock_tg.add_timer.return_value = mock_pulse + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def test_stop(self, mock_looping): + mock_timer = mock.Mock() + mock_looping.return_value = mock_timer - emitter = service_status.NoopEmitter("svc", self.mock_tg) + emitter = service_status.NoopEmitter("svc") emitter.start() emitter.stop() - self.assertFalse(emitter._running) + mock_timer.stop.assert_called_once() class RpcEmitterTest(oslotest.base.BaseTestCase): def setUp(self): super(RpcEmitterTest, self).setUp() - self.mock_tg = mock.Mock() @mock.patch.object(objects, "ServiceStatus") @mock.patch("designate.context.DesignateContext.get_admin_context") - def test_emit_no_status_factory(self, mock_context, mock_service_status): - emitter = service_status.RpcEmitter("svc", self.mock_tg) + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def test_emit_no_status_factory(self, mock_looping, mock_context, + mock_service_status): + mock_timer = mock.Mock() + mock_looping.return_value = mock_timer + + emitter = service_status.RpcEmitter("svc") emitter.start() + mock_timer.start.assert_called_once() + central = mock.Mock() with mock.patch("designate.central.rpcapi.CentralAPI.get_instance", return_value=central): @@ -76,16 +83,23 @@ class RpcEmitterTest(oslotest.base.BaseTestCase): @mock.patch.object(objects, "ServiceStatus") @mock.patch("designate.context.DesignateContext.get_admin_context") - def test_emit_status_factory(self, mock_context, mock_service_status): + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def test_emit_status_factory(self, mock_looping, mock_context, + mock_service_status): + mock_timer = mock.Mock() + mock_looping.return_value = mock_timer + status = False stats = {"a": 1} capabilities = {"b": 2} status_factory = mock.Mock(return_value=(status, stats, capabilities,)) - emitter = service_status.RpcEmitter("svc", self.mock_tg, + emitter = service_status.RpcEmitter("svc", status_factory=status_factory) emitter.start() + mock_timer.start.assert_called_once() + central = mock.Mock() with mock.patch("designate.central.rpcapi.CentralAPI.get_instance", return_value=central): 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/devstack/plugin.sh b/devstack/plugin.sh index d6f9c85c..e8a635c3 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -115,14 +115,7 @@ function configure_designate { fi # Logging Configuration - if [ "$USE_SYSTEMD" != "False" ]; then - setup_systemd_logging $DESIGNATE_CONF - fi - - # Format logging - if [ "$LOG_COLOR" == "True" ] && [ "$USE_SYSTEMD" == "False" ]; then - setup_colorized_logging $DESIGNATE_CONF DEFAULT - fi + setup_systemd_logging $DESIGNATE_CONF # Backend Plugin Configuation configure_designate_backend @@ -238,10 +231,6 @@ function create_designate_pool_configuration { # init_designate - Initialize etc. function init_designate { - # Some Designate Backends require mdns be bound to port 53, make that - # doable. - sudo setcap 'cap_net_bind_service=+ep' $(readlink -f /usr/bin/python) - # (Re)create designate database recreate_database designate utf8 @@ -259,12 +248,9 @@ function install_designate { install_apache_wsgi fi - - if is_ubuntu; then - install_package libcap2-bin - elif is_fedora; then + if is_fedora; then # bind-utils package provides `dig` - install_package libcap bind-utils + install_package bind-utils fi git_clone $DESIGNATE_REPO $DESIGNATE_DIR $DESIGNATE_BRANCH diff --git a/doc/requirements.txt b/doc/requirements.txt index 23e0f99c..fbade88b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,9 +1,9 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2;python_version>='3.4' # BSD +sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2 # BSD sphinxcontrib-httpdomain>=1.3.0 # BSD sphinxcontrib-blockdiag>=1.5.4 # BSD reno>=2.5.0 # Apache-2.0 os-api-ref>=1.4.0 # Apache-2.0 -openstackdocstheme>=1.18.1 # Apache-2.0 +openstackdocstheme>=1.31.2 # Apache-2.0 diff --git a/doc/source/admin/backends/akamai_v2.rst b/doc/source/admin/backends/akamai_v2.rst new file mode 100644 index 00000000..925f9b4f --- /dev/null +++ b/doc/source/admin/backends/akamai_v2.rst @@ -0,0 +1,40 @@ +.. + Copyright 2013 Hewlett-Packard Development Company, L.P. + + 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. + +.. _akamai_v2_backend_docs: + +Akamai v2 Backend +================= + +This page documents using the Akamai v2 backend. +The backend uses the FastDNS V2 API to create and delete zones remotely. + +Designate Configuration +----------------------- + +Example configuration required: +One section for each pool target + + .. literalinclude:: sample_yaml_snippets/akamai-v2.yaml + :language: yaml + + +Then update the pools in designate - see :ref:`designate_manage_pool` +for further details on the ``designate-manage pool`` command + +.. code-block:: console + + $ designate-manage pool update + diff --git a/doc/source/admin/backends/sample_yaml_snippets/akamai-v2.yaml b/doc/source/admin/backends/sample_yaml_snippets/akamai-v2.yaml new file mode 100644 index 00000000..ebdcc8f8 --- /dev/null +++ b/doc/source/admin/backends/sample_yaml_snippets/akamai-v2.yaml @@ -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/doc/source/admin/support-matrix.ini b/doc/source/admin/support-matrix.ini index 98393db4..1af819a2 100644 --- a/doc/source/admin/support-matrix.ini +++ b/doc/source/admin/support-matrix.ini @@ -49,6 +49,7 @@ backend-impl-pdns4=Power DNS 4 backend-impl-designate=Designate to Designate backend-impl-dynect=DynECT backend-impl-akamai=Akamai eDNS +backend-impl-akamai_v2=Akamai DNS v2 backend-impl-infoblox-xfr=Infoblox (XFR) backend-impl-nsd4=NSD4 backend-impl-agent=Agent @@ -73,7 +74,11 @@ status=untested status=untested [backends.backend-impl-akamai] -status=untested +status=known-broken +notes=Akamai has turned off the eDNS API - see https://community.akamai.com/customers/s/article/Big-Changes-Coming-to-Fast-DNS-in-2018 + +[backends.backend-impl-akamai_v2] +docs=akamai_v2_backend_docs [backends.backend-impl-agent] diff --git a/doc/source/conf.py b/doc/source/conf.py index ffa1d137..f3cdeb01 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,6 +13,9 @@ import sys, os +# oslo.config needs the following import +import designate.conf # noqa + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -43,7 +46,6 @@ extensions = ['sphinx.ext.autodoc', repository_name = 'openstack/designate' bug_project = 'designate' bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' config_generator_config_file = '../../etc/designate/designate-config-generator.conf' sample_config_basename = '_static/designate' @@ -69,17 +71,6 @@ master_doc = 'index' project = u'designate' copyright = u'2012, Managed I.T.' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -import designate.conf # noqa -from designate.version import version_info as designate_version -version = designate_version.canonical_version_string() -# The full version, including alpha/beta/rc tags. -release = designate_version.version_string_with_vcs() - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None 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..0e98199d 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 @@ -67,7 +68,7 @@ msgpack==0.5.6 munch==2.2.0 netaddr==0.7.18 netifaces==0.10.6 -openstackdocstheme==1.20.0 +openstackdocstheme==1.31.2 openstacksdk==0.12.0 os-api-ref==1.5.0 os-client-config==1.29.0 diff --git a/playbooks/legacy/designate-devstack-agent-base/post.yaml b/playbooks/legacy/designate-devstack-agent-base/post.yaml deleted file mode 100644 index e07f5510..00000000 --- a/playbooks/legacy/designate-devstack-agent-base/post.yaml +++ /dev/null @@ -1,15 +0,0 @@ -- hosts: primary - tasks: - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/logs/** - - --include=*/ - - --exclude=* - - --prune-empty-dirs diff --git a/playbooks/legacy/designate-devstack-agent-base/run.yaml b/playbooks/legacy/designate-devstack-agent-base/run.yaml deleted file mode 100644 index f5acb694..00000000 --- a/playbooks/legacy/designate-devstack-agent-base/run.yaml +++ /dev/null @@ -1,67 +0,0 @@ -- hosts: all - name: Designate devstack agent base - tasks: - - - name: Ensure legacy workspace directory - file: - path: '{{ ansible_user_dir }}/workspace' - state: directory - - - shell: - cmd: | - set -e - set -x - cat > clonemap.yaml << EOF - clonemap: - - name: openstack/devstack-gate - dest: devstack-gate - EOF - /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - https://opendev.org \ - openstack/devstack-gate - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' - - - shell: - cmd: | - set -e - set -x - cat << 'EOF' >>"/tmp/dg-local.conf" - [[local|localrc]] - enable_plugin designate https://opendev.org/openstack/designate - DESIGNATE_SERVICE_PORT_DNS=5322 - DESIGNATE_BACKEND_DRIVER=agent - DESIGNATE_AGENT_BACKEND_DRIVER={{ backend_driver }} - - EOF - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' - - - shell: - cmd: | - set -e - set -x - export PYTHONUNBUFFERED=true - - export DEVSTACK_GATE_TEMPEST=1 - export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1 - export DEVSTACK_GATE_TEMPEST_REGEX=designate - export DEVSTACK_GATE_USE_PYTHON3=True - - export PROJECTS="openstack/designate $PROJECTS" - export PROJECTS="openstack/python-designateclient $PROJECTS" - export PROJECTS="openstack/designate-dashboard $PROJECTS" - export PROJECTS="openstack/designate-tempest-plugin $PROJECTS" - - export BRANCH_OVERRIDE=default - if [ "$BRANCH_OVERRIDE" != "default" ] ; then - export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE - fi - - cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh - ./safe-devstack-vm-gate-wrap.sh - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/legacy/designate-devstack-base/post.yaml b/playbooks/legacy/designate-devstack-base/post.yaml deleted file mode 100644 index e07f5510..00000000 --- a/playbooks/legacy/designate-devstack-base/post.yaml +++ /dev/null @@ -1,15 +0,0 @@ -- hosts: primary - tasks: - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/logs/** - - --include=*/ - - --exclude=* - - --prune-empty-dirs diff --git a/playbooks/legacy/designate-devstack-base/run.yaml b/playbooks/legacy/designate-devstack-base/run.yaml deleted file mode 100644 index 3d3ee1ab..00000000 --- a/playbooks/legacy/designate-devstack-base/run.yaml +++ /dev/null @@ -1,81 +0,0 @@ -- hosts: all - name: Autoconverted job legacy-tempest-dsvm-designate-pdns4 from old job gate-tempest-dsvm-designate-pdns4-ubuntu-xenial - tasks: - - - name: Ensure legacy workspace directory - file: - path: '{{ ansible_user_dir }}/workspace' - state: directory - - - shell: - cmd: | - set -e - set -x - cat > clonemap.yaml << EOF - clonemap: - - name: openstack/devstack-gate - dest: devstack-gate - EOF - /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - https://opendev.org \ - openstack/devstack-gate - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' - - - shell: - cmd: | - set -e - set -x - export PYTHONUNBUFFERED=true - - services=rabbit,tempest,mysql,dstat,key - services+=,n-api,n-api-meta,n-cpu,n-cond,n-sch,n-crt - # placement service mandatory for nova from ocata - if [[ "stable/newton" != $ZUUL_BRANCH ]]; then - services+=,placement-api - fi - services+=,g-api,g-reg - services+=,c-sch,c-api,c-vol,c-bak - services+=,q-svc,q-dhcp,q-meta,q-agt,q-l3 - - export DEVSTACK_GATE_TEMPEST=1 - export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1 - export DEVSTACK_GATE_TEMPEST_REGEX=designate - - export DEVSTACK_LOCAL_CONFIG="enable_plugin designate https://opendev.org/openstack/designate" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"DESIGNATE_SERVICE_PORT_DNS=5322" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"DESIGNATE_BACKEND_DRIVER={{ backend }}" - - export PROJECTS="openstack/designate $PROJECTS" - export PROJECTS="openstack/python-designateclient $PROJECTS" - export PROJECTS="openstack/designate-dashboard $PROJECTS" - export PROJECTS="openstack/designate-tempest-plugin $PROJECTS" - - export BRANCH_OVERRIDE=default - if [ "$BRANCH_OVERRIDE" != "default" ] ; then - export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE - fi - - if [ "{{ identity_v3_only }}" == "1" ] ; then - export DEVSTACK_LOCAL_CONFIG+=$'\n'"ENABLE_IDENTITY_V2=False" - fi - - if [ "{{ manager_model }}" == "1" ] ; then - export DEVSTACK_LOCAL_CONFIG+=$'\n'"disable_service designate-worker designate-producer" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_service designate-pool-manager designate-zone-manager" - fi - - export OVERRIDE_ENABLED_SERVICES=$services - - if [ "{{ database }}" == "postgres" ] ; then - export DEVSTACK_GATE_POSTGRES=1 - fi - - export DEVSTACK_GATE_USE_PYTHON3=True - - cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh - ./safe-devstack-vm-gate-wrap.sh - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/legacy/grenade-devstack-designate-pdns4/run.yaml b/playbooks/legacy/grenade-devstack-designate-pdns4/run.yaml index ae522f7a..7d10569f 100644 --- a/playbooks/legacy/grenade-devstack-designate-pdns4/run.yaml +++ b/playbooks/legacy/grenade-devstack-designate-pdns4/run.yaml @@ -31,6 +31,7 @@ [[local|localrc]] DESIGNATE_SERVICE_PORT_DNS=5322 DESIGNATE_BACKEND_DRIVER=pdns4 + TEMPEST_PLUGINS=' ../designate-tempest-plugin' EOF executable: /bin/bash @@ -45,7 +46,6 @@ export DEVSTACK_GATE_USE_PYTHON3=True export DEVSTACK_GATE_TEMPEST=1 - export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1 export DEVSTACK_GATE_GRENADE=pullup export DEVSTACK_GATE_TEMPEST_REGEX="designate_tempest_plugin(?!\.tests.api.v1).*" export DEVSTACK_GATE_HORIZON=1 diff --git a/releasenotes/notes/akamai-v2-5a3edb35f59a17c2.yaml b/releasenotes/notes/akamai-v2-5a3edb35f59a17c2.yaml new file mode 100644 index 00000000..56245b0e --- /dev/null +++ b/releasenotes/notes/akamai-v2-5a3edb35f59a17c2.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added new backend driver to support the Akamai FastDNS V2 API +upgrade: + - | + The Akamai eDNS API endpoint has been removed, users should migrate to + the Akamai v2 driver for the FastDNS V2 API.
\ No newline at end of file diff --git a/releasenotes/notes/drop-py-2-7-737ea2547cb7ea06.yaml b/releasenotes/notes/drop-py-2-7-737ea2547cb7ea06.yaml index 80922804..6ed0a77a 100644 --- a/releasenotes/notes/drop-py-2-7-737ea2547cb7ea06.yaml +++ b/releasenotes/notes/drop-py-2-7-737ea2547cb7ea06.yaml @@ -1,6 +1,7 @@ --- prelude: > - Drop the Python 2.7 support. + Python 2.7 will not be maintained past 2020 and because of that we are + dropping Python 2 support. upgrade: - | Python 2.7 support has been dropped. Last release of Designate diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 67268519..8b1de7cf 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -45,7 +45,6 @@ extensions = [ repository_name = 'openstack/designate' bug_project = 'designate' bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po index 336a55b4..dcd94650 100644 --- a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: designate\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-12-25 00:23+0000\n" +"POT-Creation-Date: 2020-03-08 23:30+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -43,15 +43,12 @@ msgstr "6.0.0" msgid "7.0.0" msgstr "7.0.0" -msgid "7.0.0-17" -msgstr "7.0.0-17" +msgid "7.0.0-19" +msgstr "7.0.0-19" msgid "8.0.0" msgstr "8.0.0" -msgid "8.0.0-12" -msgstr "8.0.0-12" - msgid "9.0.0" msgstr "9.0.0" @@ -300,9 +297,6 @@ msgstr "" "alternative to specifying several topics for notification consumers in " "configuration of service that emits notifications." -msgid "Drop the Python 2.7 support." -msgstr "Drop the Python 2.7 support." - msgid "" "Fixed `bug 1768824`_ which could cause the service_statuses table to be " "flooded with duplicate service entries." 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 @@ -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 |