Switch bind9 agent to a driver based implementation.

Fixes bug #1074091
Fixes bug #1077023

Change-Id: I2d3077fcc38c33a0a4916c935ffad6ab63f73e7b
This commit is contained in:
Kiall Mac Innes 2012-11-08 20:39:22 +00:00
parent e8e457db4e
commit e199113a96
20 changed files with 437 additions and 55 deletions

View File

@ -31,7 +31,7 @@ NOTE: This is probably incomplete!
1. Open 3 consoles/screen sessions for each command:
* `./bin/moniker-api`
* `./bin/moniker-central`
* `./bin/moniker-agent-bind9`
* `./bin/moniker-agent`
1. Make use of the API..
# TODOs:

View File

@ -19,13 +19,13 @@ import eventlet
from moniker.openstack.common import log as logging
from moniker.openstack.common import service
from moniker import utils
from moniker.agent import bind9 as bind9_service
from moniker.agent import service as agent_service
eventlet.monkey_patch()
utils.read_config('moniker-agent-bind9', sys.argv[1:])
utils.read_config('moniker-agent', sys.argv[1:])
logging.setup('moniker')
launcher = service.launch(bind9_service.Service())
launcher = service.launch(agent_service.Service())
launcher.wait()

View File

@ -8,8 +8,8 @@ debug = False
# Top-level directory for maintaining moniker's state
state_path = ./var/
#
templates_path = ./templates/
# Driver used for backend communication (e.g. bind9, powerdns)
#backend_driver=bind9
# There has to be a better way to set these defaults
allowed_rpc_exception_modules = moniker.exceptions, moniker.openstack.common.exception

View File

@ -30,6 +30,4 @@ cfg.CONF.register_opts([
cfg.StrOpt('agent-topic', default='agent', help='Agent Topic'),
cfg.StrOpt('state-path', default='$pybasedir',
help='Top-level directory for maintaining moniker\'s state'),
cfg.StrOpt('templates-path', default='$pybasedir/templates',
help='Templates Path'),
])

40
moniker/agent/service.py Normal file
View File

@ -0,0 +1,40 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker.openstack.common.rpc import service as rpc_service
from moniker import backend
LOG = logging.getLogger(__name__)
class Service(rpc_service.Service):
def __init__(self, *args, **kwargs):
kwargs.update(
host=cfg.CONF.host,
topic=cfg.CONF.agent_topic,
manager=backend.get_backend()
)
super(Service, self).__init__(*args, **kwargs)
def start(self):
self.manager.start()
super(Service, self).start()
def stop(self):
super(Service, self).stop()
self.manager.stop()

View File

@ -0,0 +1,33 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 stevedore import driver
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
LOG = logging.getLogger(__name__)
BACKEND_NAMESPACE = 'moniker.backend'
cfg.CONF.register_opts([
cfg.StrOpt('backend-driver', default='bind9',
help='The backend driver to use')
])
def get_backend():
mgr = driver.DriverManager(BACKEND_NAMESPACE, cfg.CONF.backend_driver,
invoke_on_load=True)
return mgr.driver

62
moniker/backend/base.py Normal file
View File

@ -0,0 +1,62 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 abc
from moniker.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class Backend(object):
""" Base class for backend implementations """
__metaclass__ = abc.ABCMeta
@staticmethod
def register_opts(conf):
""" Register configuration options """
def start(self):
""" Hook for any necessary startup code """
pass
def stop(self):
""" Hook for any necessary shutdown code """
pass
@abc.abstractmethod
def create_domain(self, context, domain):
""" Create a DNS domain """
@abc.abstractmethod
def update_domain(self, context, domain):
""" Update a DNS domain """
@abc.abstractmethod
def delete_domain(self, context, domain):
""" Delete a DNS domain """
@abc.abstractmethod
def create_record(self, context, domain, record):
""" Create a DNS record """
@abc.abstractmethod
def update_record(self, context, domain, record):
""" Update a DNS record """
@abc.abstractmethod
def delete_record(self, context, domain, record):
""" Delete a DNS record """

View File

@ -15,32 +15,30 @@
# under the License.
import os
import subprocess
from jinja2 import Template
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker.openstack.common.rpc import service as rpc_service
from moniker.openstack.common.context import get_admin_context
from moniker import utils
from moniker.backend import base
from moniker.central import api as central_api
LOG = logging.getLogger(__name__)
cfg.CONF.register_opts([
cfg.StrOpt('rndc-path', default='/usr/sbin/rndc', help='RNDC Path'),
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 Bind9Backend(base.Backend):
def register_opts(self, conf):
conf.register_opts([
cfg.StrOpt('rndc-path', default='/usr/sbin/rndc',
help='RNDC Path'),
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 Service(rpc_service.Service):
def __init__(self, *args, **kwargs):
kwargs.update(
host=cfg.CONF.host,
topic=cfg.CONF.agent_topic
)
super(Service, self).__init__(*args, **kwargs)
def start(self):
super(Bind9Backend, self).start()
# TODO: This is a hack to ensure the data dir is 100% up to date
admin_context = get_admin_context()
@ -86,9 +84,6 @@ class Service(rpc_service.Service):
domains = central_api.get_domains(admin_context)
template_path = os.path.join(os.path.abspath(
cfg.CONF.templates_path), 'bind9-config.jinja2')
output_folder = os.path.join(os.path.abspath(cfg.CONF.state_path),
'bind9')
@ -98,8 +93,12 @@ class Service(rpc_service.Service):
output_path = os.path.join(output_folder, 'zones.config')
self._render_template(template_path, output_path, domains=domains,
state_path=os.path.abspath(cfg.CONF.state_path))
abs_state_path = os.path.abspath(cfg.CONF.state_path)
utils.render_template_to_file('bind9-config.jinja2',
output_path,
domains=domains,
state_path=abs_state_path)
def _sync_domain(self, domain):
""" Sync a single domain's zone file """
@ -117,20 +116,16 @@ class Service(rpc_service.Service):
records = central_api.get_records(admin_context, domain['id'])
template_path = os.path.join(os.path.abspath(
cfg.CONF.templates_path), 'bind9-zone.jinja2')
output_folder = os.path.join(os.path.abspath(cfg.CONF.state_path),
'bind9')
# Create the output folder tree if necessary
if not os.path.exists(output_folder):
os.makedirs(output_folder)
output_path = os.path.join(output_folder, '%s.zone' % domain['id'])
self._render_template(template_path, output_path, servers=servers,
domain=domain, records=records)
utils.render_template_to_file('bind9-zone.jinja2',
output_path,
servers=servers,
domain=domain,
records=records)
self._sync_domains()
@ -145,20 +140,10 @@ class Service(rpc_service.Service):
rndc_call.extend(['-c', cfg.CONF.rndc_config_file])
if cfg.CONF.rndc_key_file:
rndc_call.extend(['-k', c.cfg.CONF.rndc_key_file])
rndc_call.extend(['-k', cfg.CONF.rndc_key_file])
rndc_call.extend(['reload', domain['name']])
LOG.warn(rndc_call)
subprocess.call(rndc_call)
def _render_template(self, template_path, output_path, **template_context):
# TODO: Handle failures...
with open(template_path) as template_fh:
template = Template(template_fh.read())
content = template.render(**template_context)
with open(output_path, 'w') as output_fh:
output_fh.write(content)

View File

@ -0,0 +1,55 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import log as logging
from moniker.backend import base
LOG = logging.getLogger(__name__)
class FakeBackend(base.Backend):
def __init__(self, *args, **kwargs):
super(FakeBackend, self).__init__(*args, **kwargs)
self.create_domain_calls = []
self.update_domain_calls = []
self.delete_domain_calls = []
self.create_record_calls = []
self.update_record_calls = []
self.delete_record_calls = []
def create_domain(self, context, domain):
LOG.info('Create Domain %r' % domain)
self.create_domain_calls.append((context, domain))
def update_domain(self, context, domain):
LOG.debug('Update Domain %r' % domain)
self.update_domain_calls.append((context, domain))
def delete_domain(self, context, domain):
LOG.debug('Delete Domain %r' % domain)
self.delete_domain_calls.append((context, domain))
def create_record(self, context, domain, record):
LOG.debug('Create Record %r / %r' % (domain, record))
self.create_record_calls.append((context, domain, record))
def update_record(self, context, domain, record):
LOG.debug('Update Record %r / %r' % (domain, record))
self.update_record_calls.append((context, domain, record))
def delete_record(self, context, domain, record):
LOG.debug('Delete Record %r / %r' % (domain, record))
self.delete_record_calls.append((context, domain, record))

View File

@ -73,3 +73,7 @@ class DomainNotFound(NotFound):
class RecordNotFound(NotFound):
pass
class TemplateNotFound(NotFound):
pass

View File

@ -29,9 +29,7 @@ class TestCase(unittest2.TestCase):
super(TestCase, self).setUp()
self.mox = mox.Mox()
self.config(database_connection='sqlite://',
rpc_backend='moniker.openstack.common.rpc.impl_fake',
notification_driver=[])
self.config(**self.get_config_overrides())
storage.setup_schema()
self.admin_context = self.get_admin_context()
@ -48,6 +46,14 @@ class TestCase(unittest2.TestCase):
for k, v in kwargs.iteritems():
cfg.CONF.set_override(k, v, group)
def get_config_overrides(self):
return dict(
database_connection='sqlite://',
rpc_backend='moniker.openstack.common.rpc.impl_fake',
notification_driver=[],
backend_driver='fake'
)
def get_central_service(self):
return central_service.Service()

View File

@ -0,0 +1,36 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import log as logging
from moniker.tests import TestCase
from moniker import backend
LOG = logging.getLogger(__name__)
class BackendDriverTestCase(TestCase):
__test__ = False
def get_backend_driver(self):
return backend.get_backend()
def setUp(self):
super(BackendDriverTestCase, self).setUp()
self.backend = self.get_backend_driver()
def test_dummy(self):
# Right now we just check that we can instantiate the driver via the
# setUp above. Proper tests TODO.
pass

View File

@ -0,0 +1,32 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import log as logging
from moniker.tests.test_backend import BackendDriverTestCase
LOG = logging.getLogger(__name__)
class Bind9BackendDriverTestCase(BackendDriverTestCase):
__test__ = True
def get_config_overrides(self):
overrides = BackendDriverTestCase.get_config_overrides(self)
overrides.update(
backend_driver='bind9'
)
return overrides

View File

@ -0,0 +1,32 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import log as logging
from moniker.tests.test_backend import BackendDriverTestCase
LOG = logging.getLogger(__name__)
class FakeBackendDriverTestCase(BackendDriverTestCase):
__test__ = True
def get_config_overrides(self):
overrides = BackendDriverTestCase.get_config_overrides(self)
overrides.update(
backend_driver='fake'
)
return overrides

View File

@ -0,0 +1,59 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 tempfile
from jinja2 import Template
from moniker.tests import TestCase
from moniker import exceptions
from moniker import utils
class TestUtils(TestCase):
def test_load_template(self):
name = 'bind9-config.jinja2'
template = utils.load_template(name)
self.assertIsInstance(template, Template)
def test_load_template_missing(self):
name = 'invalid.jinja2'
with self.assertRaises(exceptions.TemplateNotFound):
utils.load_template(name)
def test_render_template(self):
template = Template("Hello {{name}}")
result = utils.render_template(template, name="World")
self.assertEqual('Hello World', result)
def render_template_to_file(self):
output_path = tempfile.mktemp()
template = Template("Hello {{name}}")
utils.render_template_to_file(template, output_path=output_path,
name="World")
self.assertTrue(os.path.exists(output_path))
try:
with open(output_path, 'r') as fh:
self.assertEqual('Hello World', fh.read())
finally:
os.unlink(output_path)

View File

@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import pkg_resources
from jinja2 import Template
from moniker.openstack.common import log as logging
from moniker.openstack.common import cfg
from moniker.openstack.common.notifier import api as notifier_api
@ -62,3 +64,37 @@ def read_config(prog, argv=None):
cfg.CONF(argv, project='moniker', prog=prog,
default_config_files=config_files)
def load_template(template_name):
template_path = os.path.join('resources', 'templates', template_name)
if not pkg_resources.resource_exists('moniker', template_path):
raise exceptions.TemplateNotFound('Could not find the requested '
'template: %s' % template_name)
template_string = pkg_resources.resource_string('moniker', template_path)
return Template(template_string)
def render_template(template, **template_context):
if not isinstance(template, Template):
template = load_template(template)
return template.render(**template_context)
def render_template_to_file(template_name, output_path, makedirs=True,
**template_context):
output_folder = os.path.dirname(output_path)
# Create the output folder tree if necessary
if makedirs and not os.path.exists(output_folder):
os.makedirs(output_folder)
# Render the template
content = render_template(template_name, **template_context)
with open(output_path, 'w') as output_fh:
output_fh.write(content)

View File

@ -51,7 +51,7 @@ setup(
scripts=[
'bin/moniker-central',
'bin/moniker-api',
'bin/moniker-agent-bind9',
'bin/moniker-agent',
],
cmdclass=common_setup.get_cmdclass(),
entry_points=textwrap.dedent("""
@ -63,6 +63,10 @@ setup(
[moniker.notification.handler]
nova = moniker.notification_handler.nova:NovaHandler
[moniker.backend]
bind9 = moniker.backend.impl_bind9:Bind9Backend
fake = moniker.backend.impl_fake:FakeBackend
[moniker.cli]
database init = moniker.cli.database:InitCommand
database sync = moniker.cli.database:SyncCommand

View File

@ -21,4 +21,4 @@ sitepackages = False
commands = nosetests --no-path-adjustment --with-coverage --cover-erase --cover-package=moniker --cover-inclusive []
[testenv:pep8]
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc,openstack moniker setup.py bin/moniker-api bin/moniker-central bin/moniker-agent-bind9
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc,openstack moniker setup.py bin/moniker-api bin/moniker-central bin/moniker-agent