From e6509b10e227a60602e7d5812a20f2246ca63847 Mon Sep 17 00:00:00 2001 From: Dag Stenstad Date: Tue, 28 Apr 2015 14:01:56 +0200 Subject: [PATCH] Add NSD4 backend Add a NSD4 backend that integrates with MiniDNS. Change-Id: Ic4cbe1ed99e67c8abecc02e67641a2c5b5d84a8e Co-Authored-By: Ron Rickard Co-Authored-By: Artom Lifshitz Co-Authored-By: Endre Karlson --- contrib/archive/backends/impl_nsd4slave.py | 140 --------------------- designate/backend/impl_nsd4.py | 103 +++++++++++++++ designate/tests/test_backend/test_nsd4.py | 138 ++++++++++++++++++++ setup.cfg | 1 + 4 files changed, 242 insertions(+), 140 deletions(-) delete mode 100644 contrib/archive/backends/impl_nsd4slave.py create mode 100644 designate/backend/impl_nsd4.py create mode 100644 designate/tests/test_backend/test_nsd4.py diff --git a/contrib/archive/backends/impl_nsd4slave.py b/contrib/archive/backends/impl_nsd4slave.py deleted file mode 100644 index e48e780ca..000000000 --- a/contrib/archive/backends/impl_nsd4slave.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2013 eNovance SAS -# -# Author: Artom Lifshitz -# -# 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 os -import socket -import ssl - -import eventlet -from oslo_config import cfg -from oslo_log import log as logging - -from designate import exceptions -from designate import utils -from designate.backend import base - - -LOG = logging.getLogger(__name__) - -CFG_GROUP = 'backend:nsd4slave' -DEFAULT_PORT = 8952 - - -class NSD4SlaveBackend(base.Backend): - __plugin__name__ = 'nsd4slave' - NSDCT_VERSION = 'NSDCT1' - - @classmethod - def get_cfg_opts(cls): - group = cfg.OptGroup( - name=CFG_GROUP, title="Configuration for NSD4-slave backend" - ) - - opts = [ - cfg.StrOpt('keyfile', default='/etc/nsd/nsd_control.key', - help='Keyfile to use when connecting to the NSD4 ' - 'servers over SSL'), - cfg.StrOpt('certfile', default='/etc/nsd/nsd_control.pem', - help='Certfile to use when connecting to the NSD4 ' - 'servers over SSL'), - cfg.ListOpt('servers', - help='Comma-separated list of servers to control, in ' - ' : format. If is omitted, ' - ' the default 8952 is used.'), - cfg.StrOpt('pattern', default='slave', - help='Pattern to use when creating zones on the NSD4 ' - 'servers. This pattern must be identically ' - 'configured on all NSD4 servers.'), - ] - - return [(group, opts)] - - def __init__(self, central_service): - self._keyfile = cfg.CONF[CFG_GROUP].keyfile - self._certfile = cfg.CONF[CFG_GROUP].certfile - # Make sure keyfile and certfile are readable to avoid cryptic SSL - # errors later - if not os.access(self._keyfile, os.R_OK): - raise exceptions.NSD4SlaveBackendError( - 'Keyfile %s missing or permission denied' % self._keyfile) - if not os.access(self._certfile, os.R_OK): - raise exceptions.NSD4SlaveBackendError( - 'Certfile %s missing or permission denied' % self._certfile) - self._pattern = cfg.CONF[CFG_GROUP].pattern - try: - self._servers = [self._parse_server(cfg_server) - for cfg_server in cfg.CONF[CFG_GROUP].servers] - except TypeError: - raise exceptions.ConfigurationError('No NSD4 servers defined') - - def _parse_server(self, cfg_server): - try: - (host, port) = utils.split_host_port(cfg_server) - port = int(port) - except ValueError: - host = str(cfg_server) - port = DEFAULT_PORT - return {'host': host, 'port': port} - - def create_domain(self, context, domain): - command = 'addzone %s %s' % (domain['name'], self._pattern) - self._all_servers(command) - - def update_domain(self, context, domain): - pass - - def delete_domain(self, context, domain): - command = 'delzone %s' % domain['name'] - self._all_servers(command) - - def _all_servers(self, command): - for server in self._servers: - try: - result = self._command(command, server['host'], server['port']) - except (ssl.SSLError, socket.error) as e: - raise exceptions.NSD4SlaveBackendError(e) - if result != 'ok': - raise exceptions.NSD4SlaveBackendError(result) - - def _command(self, command, host, port): - sock = eventlet.wrap_ssl(eventlet.connect((host, port)), - keyfile=self._keyfile, - certfile=self._certfile) - stream = sock.makefile() - stream.write('%s %s\n' % (self.NSDCT_VERSION, command)) - stream.flush() - result = stream.read() - stream.close() - sock.close() - return result.rstrip() - - def create_recordset(self, context, domain, recordset): - pass - - def update_recordset(self, context, domain, recordset): - pass - - def delete_recordset(self, context, domain, recordset): - pass - - def create_record(self, context, domain, recordset, record): - pass - - def update_record(self, context, domain, recordset, record): - pass - - def delete_record(self, context, domain, recordset, record): - pass diff --git a/designate/backend/impl_nsd4.py b/designate/backend/impl_nsd4.py new file mode 100644 index 000000000..cb72d8a3a --- /dev/null +++ b/designate/backend/impl_nsd4.py @@ -0,0 +1,103 @@ +# Copyright (C) 2013 eNovance SAS +# Copyright 2014 eBay Inc. +# Copyright 2015 Zetta.IO. +# +# Author: Ron Rickard +# Author: Artom Lifshitz +# Author: Dag Stenstad +# +# 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 random +import socket +import ssl + +import eventlet +from oslo_log import log as logging + +from designate import exceptions +from designate.backend import base + + +LOG = logging.getLogger(__name__) + + +class NSD4Backend(base.Backend): + __plugin_name__ = 'nsd4' + NSDCT_VERSION = 'NSDCT1' + + def __init__(self, target): + super(NSD4Backend, self).__init__(target) + + self.host = self.options.get('host', '127.0.0.1') + self.port = int(self.options.get('port', 8952)) + self.certfile = self.options.get('certfile', + '/etc/nsd/nsd_control.pem') + self.keyfile = self.options.get('keyfile', + '/etc/nsd/nsd_control.key') + self.pattern = self.options.get('pattern', 'slave') + + def _command(self, command): + sock = eventlet.wrap_ssl( + eventlet.connect((self.host, self.port)), + keyfile=self.keyfile, + certfile=self.certfile) + stream = sock.makefile() + stream.write('%s %s\n' % (self.NSDCT_VERSION, command)) + stream.flush() + result = stream.read() + stream.close() + sock.close() + return result + + def _execute_nsd4(self, command): + try: + LOG.debug('Executing NSD4 control call: %s on %s' % (command, + self.host)) + result = self._command(command) + except (ssl.SSLError, socket.error) as e: + LOG.debug('NSD4 control call failure: %s' % e) + raise exceptions.Backend(e) + if result != 'ok': + raise exceptions.Backend(result) + + def create_domain(self, context, domain): + LOG.debug('Create Domain') + masters = [] + for master in self.masters: + host = master['host'] + port = master['port'] + masters.append('%s port %s' % (host, port)) + + # Ensure different MiniDNS instances are targetted for AXFRs + random.shuffle(masters) + + command = 'addzone %s %s' % (domain['name'], self.pattern) + + try: + self._execute_nsd4(command) + except exceptions.Backend as e: + # If create fails because the domain exists, don't reraise + if "already exists" not in str(e.message): + raise + + def delete_domain(self, context, domain): + LOG.debug('Delete Domain') + command = 'delzone %s' % domain['name'] + + try: + self._execute_nsd4(command) + except exceptions.Backend as e: + # If domain is already deleted, don't reraise + if "not found" not in str(e.message): + raise diff --git a/designate/tests/test_backend/test_nsd4.py b/designate/tests/test_backend/test_nsd4.py new file mode 100644 index 000000000..f708ab435 --- /dev/null +++ b/designate/tests/test_backend/test_nsd4.py @@ -0,0 +1,138 @@ +# Copyright (C) 2013 eNovance SAS +# +# Author: Artom Lifshitz +# +# 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 os +import socket +import ssl + +import eventlet +import fixtures +from mock import MagicMock + +from designate import exceptions +from designate import objects +from designate.tests.test_backend import BackendTestCase +from designate.tests import resources +from designate.backend import impl_nsd4 + + +class NSD4ServerStub: + recved_command = None + response = 'ok' + keyfile = os.path.join(resources.path, 'ssl', 'nsd_server.key') + certfile = os.path.join(resources.path, 'ssl', 'nsd_server.pem') + + def handle(self, client_sock, client_addr): + stream = client_sock.makefile() + self.recved_command = stream.readline() + stream.write(self.response) + stream.flush() + + def start(self): + self.port = 1025 + while True: + try: + eventlet.spawn_n(eventlet.serve, + eventlet.wrap_ssl( + eventlet.listen(('127.0.0.1', self.port)), + keyfile=self.keyfile, + certfile=self.certfile, + server_side=True), + self.handle) + break + except socket.error: + self.port = self.port + 1 + + def stop(self): + eventlet.StopServe() + + +class NSD4Fixture(fixtures.Fixture): + def setUp(self): + super(NSD4Fixture, self).setUp() + self.server = NSD4ServerStub() + self.server.start() + + self.addCleanup(self.tearDown) + + def tearDown(self): + self.server.stop() + + +# NOTE: We'll only test the specifics to the nsd4 backend here. +# Rest is handled via scenarios +class NSD4BackendTestCase(BackendTestCase): + def setUp(self): + super(NSD4BackendTestCase, self).setUp() + + self.server_fixture = NSD4Fixture() + self.useFixture(self.server_fixture) + + keyfile = os.path.join(resources.path, 'ssl', 'nsd_control.key') + certfile = os.path.join(resources.path, 'ssl', 'nsd_control.pem') + + self.target = objects.PoolTarget.from_dict({ + 'id': '4588652b-50e7-46b9-b688-a9bad40a873e', + 'type': 'nsd4', + 'masters': [{'host': '192.0.2.1', 'port': 53}, + {'host': '192.0.2.2', 'port': 35}], + 'options': [ + {'key': 'keyfile', 'value': keyfile}, + {'key': 'certfile', 'value': certfile}, + {'key': 'pattern', 'value': 'test-pattern'}, + {'key': 'port', 'value': self.server_fixture.server.port} + ], + }) + + self.backend = impl_nsd4.NSD4Backend(self.target) + + def test_create_domain(self): + context = self.get_context() + domain = self.get_domain_fixture() + self.backend.create_domain(context, domain) + command = 'NSDCT1 addzone %s test-pattern\n' % domain['name'] + self.assertEqual(command, self.server_fixture.server.recved_command) + + def test_delete_domain(self): + context = self.get_context() + domain = self.get_domain_fixture() + self.backend.delete_domain(context, domain) + command = 'NSDCT1 delzone %s\n' % domain['name'] + self.assertEqual(command, self.server_fixture.server.recved_command) + + def test_server_not_ok(self): + self.server_fixture.server.response = 'goat' + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.Backend, + self.backend.create_domain, + context, domain) + + def test_ssl_error(self): + self.backend._command = MagicMock(side_effect=ssl.SSLError) + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.Backend, + self.backend.create_domain, + context, domain) + + def test_socket_error(self): + self.backend._command = MagicMock(side_effect=socket.error) + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.Backend, + self.backend.create_domain, + context, domain) diff --git a/setup.cfg b/setup.cfg index 2f7e940fa..f6129abb6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -83,6 +83,7 @@ designate.backend = powerdns = designate.backend.impl_powerdns:PowerDNSBackend dynect = designate.backend.impl_dynect:DynECTBackend akamai = designate.backend.impl_akamai:AkamaiBackend + nsd4 = designate.backend.impl_nsd4:NSD4Backend fake = designate.backend.impl_fake:FakeBackend agent = designate.backend.agent:AgentPoolBackend