diff --git a/contrib/pmdomain.py b/contrib/pmdomain.py new file mode 100644 index 000000000..429171937 --- /dev/null +++ b/contrib/pmdomain.py @@ -0,0 +1,86 @@ +# Copyright (C) 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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 sys +import getopt + +import eventlet +from oslo.config import cfg + +from designate import utils +from designate.pool_manager import rpcapi +from designate.context import DesignateContext +from designate.objects import Domain +from designate import rpc + + +# This is required to ensure ampq works without hanging. +eventlet.monkey_patch(os=False) + + +def main(argv): + # TODO(Ron): remove this application once unit testing is in place. + + usage = 'pmdomain.py -c | -d ' + domain_name = None + create = False + delete = False + + try: + opts, args = getopt.getopt( + argv, "hc:d:", ["help", "create=", "delete="]) + except getopt.GetoptError: + print('%s' % usage) + sys.exit(2) + + for opt, arg in opts: + if opt in ("-h", "--help"): + print('%s' % usage) + sys.exit() + elif opt in ("-c", "--create"): + create = True + domain_name = arg + elif opt in ("-d", "--delete"): + delete = True + domain_name = arg + + if (delete and create) or (not delete and not create): + print('%s' % usage) + sys.exit(2) + + # Read the Designate configuration file. + utils.read_config('designate', []) + rpc.init(cfg.CONF) + + context = DesignateContext.get_admin_context( + tenant=utils.generate_uuid(), + user=utils.generate_uuid()) + pool_manager_api = rpcapi.PoolManagerAPI() + + # For the BIND9 backend, all that is needed is a name. + values = { + 'name': domain_name + } + domain = Domain(**values) + + if create: + pool_manager_api.create_domain(context, domain) + + if delete: + pool_manager_api.delete_domain(context, domain) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/designate/__init__.py b/designate/__init__.py index 5be5b395b..604f64dbe 100644 --- a/designate/__init__.py +++ b/designate/__init__.py @@ -39,6 +39,8 @@ cfg.CONF.register_opts([ cfg.StrOpt('central-topic', default='central', help='Central Topic'), cfg.StrOpt('agent-topic', default='agent', help='Agent Topic'), cfg.StrOpt('mdns-topic', default='mdns', help='mDNS Topic'), + cfg.StrOpt('pool-manager-topic', default='pool_manager', + help='Pool Manager Topic'), # Default TTL cfg.IntOpt('default-ttl', default=3600), diff --git a/designate/backend/__init__.py b/designate/backend/__init__.py index 372eabd81..a86bc1a3d 100644 --- a/designate/backend/__init__.py +++ b/designate/backend/__init__.py @@ -15,6 +15,7 @@ # under the License. from designate.openstack.common import log as logging from designate.backend.base import Backend +from designate.backend.base import PoolBackend LOG = logging.getLogger(__name__) @@ -25,3 +26,18 @@ def get_backend(backend_driver, central_service): cls = Backend.get_driver(backend_driver) return cls(central_service=central_service) + + +def get_server_object(backend_driver, server_id): + LOG.debug("Loading pool backend driver: %s" % backend_driver) + + cls = PoolBackend.get_driver(backend_driver) + return cls.get_server_object(backend_driver, server_id) + + +def get_pool_backend(backend_driver, backend_options): + LOG.debug("Loading pool backend driver: %s" % backend_driver) + + cls = PoolBackend.get_driver(backend_driver) + + return cls(backend_options) diff --git a/designate/backend/base.py b/designate/backend/base.py index 3f1f1fed4..a0bfa8c60 100644 --- a/designate/backend/base.py +++ b/designate/backend/base.py @@ -15,11 +15,15 @@ # under the License. import abc +from oslo.config import cfg + +import designate.pool_manager.backend_section_name as backend_section_name from designate.openstack.common import log as logging from designate.i18n import _LW from designate import exceptions from designate.context import DesignateContext from designate.plugin import DriverPlugin +from designate import objects LOG = logging.getLogger(__name__) @@ -155,3 +159,170 @@ class Backend(DriverPlugin): return { 'status': None } + + +class PoolBackend(Backend): + + def __init__(self, backend_options): + super(PoolBackend, self).__init__(None) + self.backend_options = backend_options + + @classmethod + def _create_server_object(cls, backend, server_id, backend_options, + server_section_name): + """ + Create the server object. + """ + server_values = { + 'id': server_id, + 'host': cfg.CONF[server_section_name].host, + 'port': cfg.CONF[server_section_name].port, + 'backend': backend, + 'backend_options': backend_options, + 'tsig_key': cfg.CONF[server_section_name].tsig_key + } + return objects.PoolServer(**server_values) + + @classmethod + def _create_backend_option_objects(cls, global_section_name, + server_section_name): + """ + Create the backend_option object list. + """ + backend_options = [] + for key in cfg.CONF[global_section_name].keys(): + backend_option = cls._create_backend_option_object( + key, global_section_name, server_section_name) + backend_options.append(backend_option) + return backend_options + + @classmethod + def _create_backend_option_object(cls, key, global_section_name, + server_section_name): + """ + Create the backend_option object. If a server specific backend option + value exists, use it. Otherwise use the global backend option value. + """ + value = cfg.CONF[server_section_name].get(key) + if value is None: + value = cfg.CONF[global_section_name].get(key) + backend_option_values = { + 'key': key, + 'value': value + } + return objects.BackendOption(**backend_option_values) + + @classmethod + def _register_opts(cls, backend, server_id): + """ + Register the global and server specific backend options. + """ + global_section_name = backend_section_name \ + .generate_global_section_name(backend) + server_section_name = backend_section_name \ + .generate_server_section_name(backend, server_id) + + # Register the global backend options. + global_opts = cls.get_cfg_opts() + cfg.CONF.register_group(cfg.OptGroup(name=global_section_name)) + cfg.CONF.register_opts(global_opts, group=global_section_name) + + # Register the server specific backend options. + server_opts = global_opts + server_opts.append(cfg.StrOpt('host', help='Server Host')) + server_opts.append(cfg.IntOpt('port', default=53, help='Server Port')) + server_opts.append(cfg.StrOpt('tsig-key', help='Server TSIG Key')) + cfg.CONF.register_group(cfg.OptGroup(name=server_section_name)) + cfg.CONF.register_opts(server_opts, group=server_section_name) + + # Ensure the server specific backend options do not have a default + # value. This is necessary so the default value does not override + # a global backend option value set in the configuration file. + for key in cfg.CONF[global_section_name].keys(): + cfg.CONF.set_default(key, None, group=server_section_name) + + return global_section_name, server_section_name + + @abc.abstractmethod + def get_cfg_opts(self): + """ + Get the configuration options. + """ + + @classmethod + def get_server_object(cls, backend, server_id): + """ + Get the server object from the backend driver for the server_id. + """ + global_section_name, server_section_name = cls._register_opts( + backend, server_id) + + backend_options = cls._create_backend_option_objects( + global_section_name, server_section_name) + + return cls._create_server_object( + backend, server_id, backend_options, server_section_name) + + def get_backend_option(self, key): + """ + Get the backend option value using the backend option key. + """ + for backend_option in self.backend_options: + if backend_option['key'] == key: + return backend_option['value'] + + def create_tsigkey(self, context, tsigkey): + pass + + def update_tsigkey(self, context, tsigkey): + pass + + def delete_tsigkey(self, context, tsigkey): + pass + + def create_server(self, context, server): + pass + + def update_server(self, context, server): + pass + + def delete_server(self, context, server): + pass + + @abc.abstractmethod + def create_domain(self, context, domain): + """Create a DNS domain""" + + def update_domain(self, context, domain): + pass + + @abc.abstractmethod + def delete_domain(self, context, domain): + """Delete a DNS domain""" + + 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 + + def sync_domain(self, context, domain, records): + pass + + def sync_record(self, context, domain, record): + pass + + def ping(self, context): + pass diff --git a/designate/backend/impl_bind9_pool.py b/designate/backend/impl_bind9_pool.py new file mode 100644 index 000000000..602c6e442 --- /dev/null +++ b/designate/backend/impl_bind9_pool.py @@ -0,0 +1,80 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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. +from oslo.config import cfg + +from designate.openstack.common import log as logging +from designate import utils +from designate.backend import base + + +LOG = logging.getLogger(__name__) + +cfg_opts = [ + cfg.ListOpt('masters', help="Master servers from which to transfer from"), + cfg.StrOpt('rndc-host', default='127.0.0.1', help='RNDC Host'), + cfg.IntOpt('rndc-port', default=953, help='RNDC Port'), + cfg.StrOpt('rndc-config-file', default=None, help='RNDC Config File'), + cfg.StrOpt('rndc-key-file', default=None, help='RNDC Key File'), +] + + +class Bind9PoolBackend(base.PoolBackend): + __plugin_name__ = 'bind9_pool' + + @classmethod + def get_cfg_opts(cls): + return cfg_opts + + def create_domain(self, context, domain): + LOG.debug('Create Domain') + masters = '; '.join(self.get_backend_option('masters')) + rndc_op = [ + 'addzone', + '%s { type slave; masters { %s;}; file "%s.slave"; };' % + (domain['name'], masters, domain['name']), + ] + self._execute_rndc(rndc_op) + + def delete_domain(self, context, domain): + LOG.debug('Delete Domain') + rndc_op = [ + 'delzone', + '%s' % domain['name'], + ] + self._execute_rndc(rndc_op) + + def _rndc_base(self): + rndc_call = [ + 'rndc', + '-s', self.get_backend_option('rndc_host'), + '-p', str(self.get_backend_option('rndc_port')), + ] + + if self.get_backend_option('rndc_config_file'): + rndc_call.extend( + ['-c', self.get_backend_option('rndc_config_file')]) + + if self.get_backend_option('rndc_key_file'): + rndc_call.extend( + ['-k', self.get_backend_option('rndc_key_file')]) + + return rndc_call + + def _execute_rndc(self, rndc_op): + rndc_call = self._rndc_base() + rndc_call.extend(rndc_op) + LOG.debug('Executing RNDC call: %s' % " ".join(rndc_call)) + utils.execute(*rndc_call) diff --git a/designate/cmd/pool_manager.py b/designate/cmd/pool_manager.py new file mode 100644 index 000000000..4514c3b29 --- /dev/null +++ b/designate/cmd/pool_manager.py @@ -0,0 +1,38 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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 sys + +from oslo.config import cfg + +from designate import service +from designate import utils +from designate.openstack.common import log as logging +from designate.pool_manager import service as pool_manager_service + + +CONF = cfg.CONF +CONF.import_opt('workers', 'designate.pool_manager', + group='service:pool_manager') + + +def main(): + utils.read_config('designate', sys.argv) + logging.setup('designate') + + server = pool_manager_service.Service.create( + binary='designate-pool-manager') + service.serve(server, workers=CONF['service:pool_manager'].workers) + service.wait() diff --git a/designate/exceptions.py b/designate/exceptions.py index 97d17fa9f..ce7bdfdd6 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -208,6 +208,10 @@ class DuplicateBlacklist(Duplicate): error_type = 'duplicate_blacklist' +class DuplicatePoolManagerStatus(Duplicate): + error_type = 'duplication_pool_manager_status' + + class MethodNotAllowed(Base): expected = True error_code = 405 @@ -256,6 +260,10 @@ class ReportNotFound(NotFound): error_type = 'report_not_found' +class PoolManagerStatusNotFound(NotFound): + error_type = 'pool_manager_status_not_found' + + class LastServerDeleteNotAllowed(BadRequest): error_type = 'last_server_delete_not_allowed' diff --git a/designate/manage/pool_manager_cache.py b/designate/manage/pool_manager_cache.py new file mode 100644 index 000000000..55b32d101 --- /dev/null +++ b/designate/manage/pool_manager_cache.py @@ -0,0 +1,59 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes +# +# 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 + +from migrate.versioning import api as versioning_api +from oslo.config import cfg + +from designate.openstack.common import log as logging +from designate.manage import base +from designate.sqlalchemy import utils + + +LOG = logging.getLogger(__name__) + +REPOSITORY = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', + 'pool_manager', + 'cache', 'impl_sqlalchemy', + 'migrate_repo')) +cfg.CONF.import_opt('connection', + 'designate.pool_manager.cache.impl_sqlalchemy', + group='pool_manager_cache:sqlalchemy') + +CONF = cfg.CONF + + +def get_manager(): + return utils.get_migration_manager( + REPOSITORY, CONF['pool_manager_cache:sqlalchemy'].connection) + + +class DatabaseCommands(base.Commands): + def version(self): + current = get_manager().version() + latest = versioning_api.version(repository=REPOSITORY).value + print("Current: %s Latest: %s" % (current, latest)) + + def sync(self): + get_manager().upgrade(None) + + @base.args('revision', nargs='?') + def upgrade(self, revision): + get_manager().upgrade(revision) + + @base.args('revision', nargs='?') + def downgrade(self, revision): + get_manager().downgrade(revision) diff --git a/designate/objects/__init__.py b/designate/objects/__init__.py index 10739063a..aa6e99fe8 100644 --- a/designate/objects/__init__.py +++ b/designate/objects/__init__.py @@ -17,8 +17,11 @@ from designate.objects.base import DesignateObject # noqa from designate.objects.base import DictObjectMixin # noqa from designate.objects.base import ListObjectMixin # noqa +from designate.objects.backend_option import BackendOption, BackendOptionList # noqa from designate.objects.blacklist import Blacklist, BlacklistList # noqa from designate.objects.domain import Domain, DomainList # noqa +from designate.objects.pool_manager_status import PoolManagerStatus, PoolManagerStatusList # noqa +from designate.objects.pool_server import PoolServer, PoolServerList # noqa from designate.objects.quota import Quota, QuotaList # noqa from designate.objects.rrdata_a import RRData_A # noqa from designate.objects.rrdata_aaaa import RRData_AAAA # noqa diff --git a/designate/objects/backend_option.py b/designate/objects/backend_option.py new file mode 100644 index 000000000..2a39d113d --- /dev/null +++ b/designate/objects/backend_option.py @@ -0,0 +1,27 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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. +from designate.objects import base + + +class BackendOption(base.DictObjectMixin, base.DesignateObject): + FIELDS = { + 'key': {}, + 'value': {} + } + + +class BackendOptionList(base.ListObjectMixin, base.DesignateObject): + LIST_ITEM_TYPE = BackendOption diff --git a/designate/objects/pool_manager_status.py b/designate/objects/pool_manager_status.py new file mode 100644 index 000000000..3d76706ad --- /dev/null +++ b/designate/objects/pool_manager_status.py @@ -0,0 +1,31 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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. +from designate.objects import base + + +class PoolManagerStatus(base.DictObjectMixin, base.PersistentObjectMixin, + base.DesignateObject): + FIELDS = { + 'server_id': {}, + 'domain_id': {}, + 'status': {}, + 'serial_number': {}, + 'action': {} + } + + +class PoolManagerStatusList(base.ListObjectMixin, base.DesignateObject): + LIST_ITEM_TYPE = PoolManagerStatus diff --git a/designate/objects/pool_server.py b/designate/objects/pool_server.py new file mode 100644 index 000000000..352227f6d --- /dev/null +++ b/designate/objects/pool_server.py @@ -0,0 +1,32 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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. +from designate.objects import base + + +# TODO(Ron): replace the Server object with this object. +class PoolServer(base.DictObjectMixin, base.DesignateObject): + FIELDS = { + 'id': {}, + 'host': {}, + 'port': {}, + 'backend': {}, + 'backend_options': {}, + 'tsig_key': {} + } + + +class PoolServerList(base.ListObjectMixin, base.DesignateObject): + LIST_ITEM_TYPE = PoolServer diff --git a/designate/pool_manager/__init__.py b/designate/pool_manager/__init__.py new file mode 100644 index 000000000..adb9ffd70 --- /dev/null +++ b/designate/pool_manager/__init__.py @@ -0,0 +1,46 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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. +from oslo.config import cfg + +cfg.CONF.register_group(cfg.OptGroup( + name='service:pool_manager', title="Configuration for Pool Manager Service" +)) + +OPTS = [ + cfg.IntOpt('workers', default=None, + help='Number of Pool Manager worker processes to spawn'), + cfg.StrOpt('pool-name', default='default', + help='The name of the pool managed by this instance of the ' + 'Pool Manager'), + cfg.IntOpt('threshold-percentage', default=100, + help='The percentage of servers requiring a successful update ' + 'for a domain change to be considered active'), + cfg.IntOpt('poll-timeout', default=30, + help='The time to wait for a NOTIFY response from a name ' + 'server'), + cfg.IntOpt('poll-retry-interval', default=2, + help='The time between retrying to send a NOTIFY request and ' + 'waiting for a NOTIFY response'), + cfg.IntOpt('poll-max-retries', default=3, + help='The maximum number of times minidns will retry sending ' + 'a NOTIFY request and wait for a NOTIFY response'), + cfg.IntOpt('periodic-sync-interval', default=120, + help='The time between synchronizing the servers with Storage'), + cfg.StrOpt('cache-driver', default='sqlalchemy', + help='The cache driver to use'), +] + +cfg.CONF.register_opts(OPTS, group='service:pool_manager') diff --git a/designate/pool_manager/backend_section_name.py b/designate/pool_manager/backend_section_name.py new file mode 100644 index 000000000..09d106f5e --- /dev/null +++ b/designate/pool_manager/backend_section_name.py @@ -0,0 +1,136 @@ +# Copyright 2014 eBay Inc. +# +# Author: Ron Rickard +# +# 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 re + +from oslo.config import iniparser + +from designate import utils + + +GLOBAL_SECTION_NAME_LABEL = '*' +SECTION_NAME_PREFIX = 'backend' +SECTION_NAME_SEPARATOR = ':' +UUID_PATTERN = '-'.join([ + '[0-9A-Fa-f]{8}', + '[0-9A-Fa-f]{4}', + '[0-9A-Fa-f]{4}', + '[0-9A-Fa-f]{4}', + '[0-9A-Fa-f]{12}' + +]) +SECTION_PATTERN = SECTION_NAME_SEPARATOR.join([ + '^%s' % SECTION_NAME_PREFIX, + '(.*)', + '(%s)' % UUID_PATTERN +]) +SECTION_LABELS = [ + 'backend', + 'server_id' +] + + +class SectionNameParser(iniparser.BaseParser): + """ + Used to retrieve the configuration file section names and parse the names. + """ + + def __init__(self, pattern, labels): + super(SectionNameParser, self).__init__() + self.regex = re.compile(pattern) + self.labels = labels + self.sections = [] + + def assignment(self, key, value): + pass + + def new_section(self, section): + match = self.regex.match(section) + if match: + value = { + 'name': section + } + index = 1 + for label in self.labels: + value[label] = match.group(index) + index += 1 + self.sections.append(value) + + def parse(self, filename): + with open(filename) as f: + return super(SectionNameParser, self).parse(f) + + @classmethod + def find_sections(cls, filename, pattern, labels): + parser = cls(pattern, labels) + parser.parse(filename) + + return parser.sections + + +def find_server_sections(): + """ + Find the server specific backend section names. + + A server specific backend section name is: + + [backend::] + """ + config_files = utils.find_config('designate.conf') + + all_sections = [] + for filename in config_files: + sections = SectionNameParser.find_sections( + filename, SECTION_PATTERN, SECTION_LABELS) + all_sections.extend(sections) + + return all_sections + + +def _generate_section_name(backend_driver, label): + """ + Generate the section name. + + A section name is: + + [backend::