From 4fbf0e09b15dc48a17639d797e2023d89289e86e Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Fri, 15 Nov 2013 12:54:01 -0500 Subject: [PATCH] nsd4slave backend This backend sends nsd-control commands to a remote NSD4 server. It depends on the multi backend and on another master backend, most likely PowerDNS. Change-Id: I722210de97b207d318f300092f4e3be08e894961 Implements: blueprint nsd4-slave-backend --- designate/backend/impl_nsd4slave.py | 124 ++++++++++++++++++ designate/exceptions.py | 4 + designate/tests/test_backend/nsd_control.key | 21 +++ designate/tests/test_backend/nsd_control.pem | 14 ++ designate/tests/test_backend/nsd_server.key | 21 +++ designate/tests/test_backend/nsd_server.pem | 14 ++ .../tests/test_backend/test_nsd4slave.py | 122 +++++++++++++++++ etc/designate/designate.conf.sample | 9 ++ setup.cfg | 1 + 9 files changed, 330 insertions(+) create mode 100644 designate/backend/impl_nsd4slave.py create mode 100644 designate/tests/test_backend/nsd_control.key create mode 100644 designate/tests/test_backend/nsd_control.pem create mode 100644 designate/tests/test_backend/nsd_server.key create mode 100644 designate/tests/test_backend/nsd_server.pem create mode 100644 designate/tests/test_backend/test_nsd4slave.py diff --git a/designate/backend/impl_nsd4slave.py b/designate/backend/impl_nsd4slave.py new file mode 100644 index 000000000..da0f6223e --- /dev/null +++ b/designate/backend/impl_nsd4slave.py @@ -0,0 +1,124 @@ +# 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 eventlet +import socket +import ssl +from designate import exceptions +from designate.backend import base +from designate.openstack.common import log as logging +from oslo.config import cfg + +LOG = logging.getLogger(__name__) + +CFG_GRP = 'backend:nsd4slave' + +cfg.CONF.register_group( + cfg.OptGroup(name=CFG_GRP, title='Configuration for NSD4-slave backend') +) + +cfg.CONF.register_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.'), +], group=CFG_GRP) + +DEFAULT_PORT = 8952 + + +class NSD4SlaveBackend(base.Backend): + __plugin__name__ = 'nsd4slave' + NSDCT_VERSION = 'NSDCT1' + + def __init__(self, central_service): + self._keyfile = cfg.CONF[CFG_GRP].keyfile + self._certfile = cfg.CONF[CFG_GRP].certfile + self._pattern = cfg.CONF[CFG_GRP].pattern + try: + self._servers = [self._parse_server(cfg_server) + for cfg_server in cfg.CONF[CFG_GRP].servers] + except TypeError: + raise exceptions.ConfigurationError('No NSD4 servers defined') + + def _parse_server(self, cfg_server): + try: + (host, port) = cfg_server.split(':') + 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_record(self, context, domain, record): + pass + + def update_record(self, context, domain, record): + pass + + def delete_record(self, context, domain, record): + pass + + def create_server(self, context, server): + pass + + def update_server(self, context, server): + pass + + def delete_server(self, context, server): + pass diff --git a/designate/exceptions.py b/designate/exceptions.py index 04cb0b324..498f0b74b 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -34,6 +34,10 @@ class Backend(Exception): pass +class NSD4SlaveBackendError(Backend): + pass + + class NotImplemented(Base, NotImplementedError): pass diff --git a/designate/tests/test_backend/nsd_control.key b/designate/tests/test_backend/nsd_control.key new file mode 100644 index 000000000..3ffbac08e --- /dev/null +++ b/designate/tests/test_backend/nsd_control.key @@ -0,0 +1,21 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIDfQIBAAKBwQClNN7DMvEDNh9g6KCpmBCT/liwH1CyUjwG2fEjrRaRkWztjmrK +Rfjt8xYukze1OFlaiVmOFaW0Mg5dJ8VWpusah8DzhL5sjg547ZbSfZhe/yA26hA/ +7P2MBd9Zv9w32ErYQkJzyvgKBmivU1lyGIoVEf4jDr5rGZgz9mh5hiFPfyvnV2NW +3gk22JhEltP1XB6ERzKCMlpUAL62wErOiMUJX8YBMNoZ3jgYcjGFJENqYr6BkLz5 +w5PJCGz1lN656yECAwEAAQKBwD9B3UIU0WAPazhqGoqVpVNlgoP9maKOBloBPWoR +rqCSdIkJjS5iWIyUFZxD1oLXTq9yBA55j3dN614UCmXBiCgibulPnLJoOnZnUubK +HHvTaHxeZ1Zy4iqpXPo6kpH3iSNbw1UtYJXYn9xy8lZdeMoNxMkRYSVesluIPVaA +69ro33kYUSFLRR8YQMOiHSGkOGkLu4U+7nrkbkbQjoaTv5qZquHFCnLlBpM7adYM +981fDP1dgie5qk4ND1c5nQ4AAQJhANcGNlu3yJdeK+jfpKcuHCO2+j40p9hBoZFd +oCCBjw7Q9vn4RTnVjMSGYTdBhCnLoeIuCeHySOKVf0UMOGRoOkMOUVC3U0IaOGMM +GEFDMFEEGrUbEB1n62Oq9dKMOYy3IQJhAMSwVsKuEa/6I6CdtWQv6UkVfh8aAm2M +Slj0jDYv0pX/5ciBRTsAYbwliIYHtdBoFns/vEuCxO66A1LIr5w8GuZhEdIe2jDf +Q1fErWs6mxAouw3eFW93sQzClxsbiSq0AQJgZR2rxFfZwazsUzeQc3nQi88JQOV8 +JMtAUl7H5WFnx9zmt8hTrY3KA8T5xIVPxGPZPcbOqO1J7xvEXNERV85Xz57VCHMd +eRFhgNp4MYCKIR8f/Bi87EcpP5ZuwVNl4NFhAmEAh8nqC/tasLAzeoaGnUVKfC8/ +ZD9zn6e0CFfEmQBJFU+WludQIVyxHNCYwVd/WQMTSkGFQGhmhx2af8OXIXiJZbVs +NTEhl437kxNwWlAaj3xfL0K8b7klXVbWNVu8ReQBAmEAj2o92RFlyTLbvfeXsGAs +Q5UTFDIzAS9MVMEp65HD8w6dZ02ruBre0KsIJs/6aEeGORH88IUrEKPPb0C12a0e +kftUBf5JemuDKFV/L5CGpaQyFA4HSjhzlSF5QahRU3oF +-----END RSA PRIVATE KEY----- diff --git a/designate/tests/test_backend/nsd_control.pem b/designate/tests/test_backend/nsd_control.pem new file mode 100644 index 000000000..2bb5f09df --- /dev/null +++ b/designate/tests/test_backend/nsd_control.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGzCCAUQCCQDPu3EY2nVlnDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDEwNu +c2QwHhcNMTMxMTE1MTkwMjA1WhcNMzMwODAyMTkwMjA1WjAWMRQwEgYDVQQDEwtu +c2QtY29udHJvbDCB3zANBgkqhkiG9w0BAQEFAAOBzQAwgckCgcEApTTewzLxAzYf +YOigqZgQk/5YsB9QslI8BtnxI60WkZFs7Y5qykX47fMWLpM3tThZWolZjhWltDIO +XSfFVqbrGofA84S+bI4OeO2W0n2YXv8gNuoQP+z9jAXfWb/cN9hK2EJCc8r4CgZo +r1NZchiKFRH+Iw6+axmYM/ZoeYYhT38r51djVt4JNtiYRJbT9VwehEcygjJaVAC+ +tsBKzojFCV/GATDaGd44GHIxhSRDamK+gZC8+cOTyQhs9ZTeueshAgMBAAEwDQYJ +KoZIhvcNAQELBQADgcEAtuHD0nCV7atGnipcukBpgydxwWDN7/4Mwcw1EAOQ8B2L +Z4ckFRKXzgtlkg98FgvEcELBUK6TqrpEhYs0ADfPogyFhcwozqmngFHXY21caHgL +FA56nebJysDH+AoyMEJHpEDqPZA/ZIwB7XsmPFs71USiqnihhfUGA01TxF1P853m +yd7iIx926gCHvzbReJBbnxdnzcjon4m13/O8y7sBGCRiw/orKUe4CW6ubAKfwpQF +qGDccgTl9x9R6EGRW5bx +-----END CERTIFICATE----- diff --git a/designate/tests/test_backend/nsd_server.key b/designate/tests/test_backend/nsd_server.key new file mode 100644 index 000000000..2ee5b2de8 --- /dev/null +++ b/designate/tests/test_backend/nsd_server.key @@ -0,0 +1,21 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIDfQIBAAKBwQDIAkIPjr2dajz/LwViY9w7OgT4eR8FhL/KwTjh4oMHpUosMyJl +12nyp/3ylCjJLJAcTfsmDUhvQKw5o+0hUOv/FBfJ43hotdn1YLq0iD+UUhzhmavV +baMM0shPrRGYDf/mHkGDkS11qsXRrUlxWr1/qjUf6ss/CbnvwQtyL/76TAtNl5Zi +MQCqrGbXmA6NOgpH51W048FD6hg0KlXEAgKV2dm+wRMNSY0Wy7b3di21bhuvst2r +5MXFxqsXTWK65qcCAwEAAQKBwQCyo8mrKZvUSHpYlf/iJD0lLSoZX91ESZAgITqU +DoNnxcsc9UMI4UEA+ejIzGotXL8OeNwT3ZNPwtzQ0shOlH9koeapbKE0LRCSqKW5 +72OSL1Eacu3WQAl8v5soBvWK8RyFQCThWG9KJexh7aGjEakeMrFBSOz7PcMAY71f +2sher/Yk71DqbB/GwuBZZ5VIr3IoDrhz35URJtvzDfJ1KdIwUbKb3b54Rn61qx/i +MQiqC1EA8EwqnUtNVObJKx5L7hkCYQDrSddKhAK02zHYCH8ah1mx6qjiKxWHRNiX +PnSo4Z58jGg3ygbOTRXh+EbwbGwhF1uqdD1EQ8+uHOqaS8Nmscr/quW0zGQFMDzu +55xmmXxBQmul+6TUmT1YErENSbsLB2UCYQDZnWaU/j30VoKDO4F8zbHe1O6Yinq5 +P5gIbLJQi7RxIlQ/8YvYCqgCuSzr6nrqdH83vbsGWGqIvQExgrSELLQCwB8fqOGv +U6M0+0NeaQmyU4Q3AGF0CH4eGLkY8Pg4MxsCYF1dodbp54EI8hB93qIST59wNK/6 +1MImqaPqnrRdQ1y9AqYQvv6iTCqtMMk63PWMpU1QbvdlyUWYJ+guZE2eA6XMlPZX +nOEKMUPEGKYGBe6HgtwMrW3HmTYXxLY3KcfImQJgIagObYzE2D1pAhL5++t0Txpv +rHf+cxg601K8YWi6B6VfkmQxVMCRK5qoL/Sb/hb2dhCKFHkoQO26eYXVlXu0e5hr +N+JOxWcSHuedi8SDE2mHUVpluCR4HP+F4S2jtk1xAmEAoii5Rghoh/MMzHdn2loE +pTZAy0mM+Iw5Cba3QXfGA1wrM/Kyg/nJm8LYZqiRzR8smOiM3TL7+qBtN2LA36Pb +n4ryn2L3f5dBOYYuESUpmy/HHsiuzZU3Ljt333tse9cg +-----END RSA PRIVATE KEY----- diff --git a/designate/tests/test_backend/nsd_server.pem b/designate/tests/test_backend/nsd_server.pem new file mode 100644 index 000000000..ba2ba742a --- /dev/null +++ b/designate/tests/test_backend/nsd_server.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICEzCCATwCCQDmQwk4gt2rdDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDEwNu +c2QwHhcNMTMxMTE1MTkwMjA1WhcNMzMwODAyMTkwMjA1WjAOMQwwCgYDVQQDEwNu +c2Qwgd8wDQYJKoZIhvcNAQEBBQADgc0AMIHJAoHBAMgCQg+OvZ1qPP8vBWJj3Ds6 +BPh5HwWEv8rBOOHigwelSiwzImXXafKn/fKUKMkskBxN+yYNSG9ArDmj7SFQ6/8U +F8njeGi12fVgurSIP5RSHOGZq9VtowzSyE+tEZgN/+YeQYORLXWqxdGtSXFavX+q +NR/qyz8Jue/BC3Iv/vpMC02XlmIxAKqsZteYDo06CkfnVbTjwUPqGDQqVcQCApXZ +2b7BEw1JjRbLtvd2LbVuG6+y3avkxcXGqxdNYrrmpwIDAQABMA0GCSqGSIb3DQEB +CwUAA4HBAFKKQBBSAZXQaHTdRl/MuBOzIqCkzOIk0igTOLBYdwGO/xx2moo77CgD +VsYS5gHl4w211d0B8IMQKWKcNfjiPJyAOCdMGS/8NAK3Bz1umLHQn/C8ScOB0Xzc +kwUdewVTNo+7kEu8HgkJhlkXgP/PKSseZ+R8VD3ozlFQS8uojmQjzMs5ne1zkph2 +KiRTZ/JvM0Q7c3RTq24JasobOQ5Rih7tFhbqCOw0DaFhEggzNwzRgonPfdW3Ntgp +v2ZWOfkYNQ== +-----END CERTIFICATE----- diff --git a/designate/tests/test_backend/test_nsd4slave.py b/designate/tests/test_backend/test_nsd4slave.py new file mode 100644 index 000000000..8e0288e0d --- /dev/null +++ b/designate/tests/test_backend/test_nsd4slave.py @@ -0,0 +1,122 @@ +# 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 eventlet +import os +import socket +import ssl +from designate import exceptions +from designate.tests.test_backend import BackendTestCase +from mock import MagicMock +# impl_nsd4slave needs to register its options before being instanciated. +# Import it and pretend to use it to avoid flake8 unused import errors. +from designate.backend import impl_nsd4slave +impl_nsd4slave + + +class NSD4ServerStub: + recved_command = None + response = 'ok' + keyfile = os.path.join(os.path.dirname(__file__), 'nsd_server.key') + certfile = os.path.join(os.path.dirname(__file__), '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 NSD4SlaveBackendTestCase(BackendTestCase): + __test__ = True + + def setUp(self): + super(NSD4SlaveBackendTestCase, self).setUp() + self.servers = [NSD4ServerStub(), NSD4ServerStub()] + [server.start() for server in self.servers] + impl_nsd4slave.DEFAULT_PORT = self.servers[0].port + self.config(backend_driver='nsd4slave', group='service:agent') + self.config( + servers=['127.0.0.1', '127.0.0.1:%d' % self.servers[1].port], + group='backend:nsd4slave') + keyfile = os.path.join(os.path.dirname(__file__), 'nsd_control.key') + certfile = os.path.join(os.path.dirname(__file__), 'nsd_control.pem') + self.config(keyfile=keyfile, group='backend:nsd4slave') + self.config(certfile=certfile, group='backend:nsd4slave') + self.config(pattern='test-pattern', group='backend:nsd4slave') + self.nsd4 = self.get_backend_driver() + + def tearDown(self): + super(NSD4SlaveBackendTestCase, self).tearDown() + [server.stop() for server in self.servers] + + def test_create_domain(self): + context = self.get_context() + domain = self.get_domain_fixture() + self.nsd4.create_domain(context, domain) + command = 'NSDCT1 addzone %s test-pattern\n' % domain['name'] + [self.assertEqual(server.recved_command, command) + for server in self.servers] + + def test_delete_domain(self): + context = self.get_context() + domain = self.get_domain_fixture() + self.nsd4.delete_domain(context, domain) + command = 'NSDCT1 delzone %s\n' % domain['name'] + [self.assertEqual(server.recved_command, command) + for server in self.servers] + + def test_server_not_ok(self): + self.servers[0].response = 'goat' + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.NSD4SlaveBackendError, + self.nsd4.create_domain, + context, domain) + + def test_ssl_error(self): + self.nsd4._command = MagicMock(side_effet=ssl.SSLError) + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.NSD4SlaveBackendError, + self.nsd4.create_domain, + context, domain) + + def test_socket_error(self): + self.nsd4._command = MagicMock(side_effet=socket.error) + context = self.get_context() + domain = self.get_domain_fixture() + self.assertRaises(exceptions.NSD4SlaveBackendError, + self.nsd4.create_domain, + context, domain) diff --git a/etc/designate/designate.conf.sample b/etc/designate/designate.conf.sample index 8adbf2aef..5f63c06d2 100644 --- a/etc/designate/designate.conf.sample +++ b/etc/designate/designate.conf.sample @@ -166,3 +166,12 @@ root_helper = sudo #idle_timeout = 3600 #max_retries = 10 #retry_interval = 10 + +#----------------------- +# NSD4Slave Backend +#----------------------- +[backend:nsd4slave] +#keyfile=/etc/nsd/nsd_control.key +#certfile=/etc/nsd/nsd_control.pem +#servers=127.0.0.1,127.0.1.1:4242 +#pattern=slave diff --git a/setup.cfg b/setup.cfg index 0719150f4..1f73ab0a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,6 +67,7 @@ designate.backend = rpc = designate.backend.impl_rpc:RPCBackend dnsmasq = designate.backend.impl_dnsmasq:DnsmasqBackend fake = designate.backend.impl_fake:FakeBackend + nsd4slave = designate.backend.impl_nsd4slave:NSD4SlaveBackend designate.quota = noop = designate.quota.impl_noop:NoopQuota