This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/legalcode
Hook Points API¶
Designate, by design, is required to interface with legacy DNS applications. Organizations will typically have other systems that must interface with DNS and vice versa. The hook points API provides a generic way to add organization specific code that adds no overhead in the default use case and does not allow changes to the underlying APIs when code is hooked in to a method or function call.
Problem description¶
Currently, in order to support organization specific functionality, it is required to either:
Maintain a fork of the Designate code base
Monkey patch the code via a module
Treat designate as a black box and use HTTP level tools to add org specific code.
While maintaining a fork can be a reasonable solution, if there are large changes, it can be very difficult to merge upstream changes. Monkey patching forces the same level of knowledge regarding the code base, yet it is tricky to get right as small code changes can silently fail due to import changes. Lastly, treating Designate as a black box will often require org specific code to reimplement aspects of Designate due to limited level of granularity.
The Hook Points API provides a practical supported means of injecting code, while avoiding the pitfalls mentioned above.
Proposed change¶
The Hook Points API provides a decorator to define a function or method as a hook point.
@hookpoints.hook_point('pool_manager_create_domain')
def create_domain(self, context, domain):
...
In the above case the hook point is a named hook point called pool_manager_create_domain. When a name is not provided, the hook point uses the path of the module and method.
@hookpoints.hook_point()
def create_domain(self, context, domain):
...
The name is the combination of function’s module and name.
'%s.%s' % (func.__module__, func.__name__)
If no hook point is defined, the original function is returned as-is.
In order to define a hook point, a package must be installed that provides a designate.hook_point entry point.
from setuptools import setup
setup(
name='raxdns',
entry_points: {
'designate.hook_point': [
'pool_manager_create_domain = raxdns.hooks.pool_manager:create_domain'
]
}
)
When the package is installed, the hook point is then active meaning that it will be called when the target is called. It may be disabled via the configuration if necessary.
Hook Point Implementation¶
A hook point, as it will be applied as a decorator, is an object that implements a __call__ method that accepts a single function as the argument and returns an appropriate function as the result. It is up to the implementor to ensure the hook point correctly implements the API of the hook target.
For convenience, there is BaseHook that can be used to reuse common patterns.
class BaseHook(object):
OPTS = [
cfg.BoolOpt('disabled', default=False)
]
def __init__(self, group):
self.group = group
@property
def disabled(self):
return cfg.CONF[self.group].get('disabled', False)
def wrapper(self, *args, **kw):
return self.hook_target(*args, **kw)
def __call__(self, f):
# Save our hook target as an attribute for our wrapper method
self.hook_target = f
@functools.wraps(self.hook_target)
def wrapper(*args, **kw):
if self.disabled:
return self.hook_target(*args, **kw)
return self.hook(*args, **kw)
return wrapper
The BaseHook takes care of:
using functools.wrap correctly
disabling the hook when configured to do so
setting the config group for later use of the config
simplifying the decorator implementation
This base class is meant to make development of a hook simpler. A hook author is free to implement the hook as a normal decorator as well.
Configuration¶
It is important to note that any configuration must be accessed via oslo.config. The reason being is that the hooks are applied at import time, where the confguration is typically loaded at run time. Therefore, the hook may not have access to config data until the hook target is actually called.
Hook Example¶
Here is an example of a hook point that wraps the create_domain method in the Pool Manager service. It validates the domain doesn’t exist in another application that can also manage domains via the same backends.
import requests
from oslo_log import log as logging
from oslo_config import cfg
from designate.pool_manager.service import ERROR_STATUS
from designate.hookpoints import BaseHook
LOG = logging.getLogger(__name__)
class CheckDCXDomainHook(BaseHook):
OPTS = BaseHook.OPTS + [
cfg.Opt('legacy_dns', required=True),
]
@property
def sess(self):
if not hasattr(self, '_sess'):
sess = requests.Session()
self._sess = sess
return self._sess
@property
def legacy_dns(self):
return cfg.CONF[self.group].legacy_dns
def hook(self, obj, context, domain):
resp = self.sess.get(self.legacy_dns + '/find_domains?name=%s' % domain.name)
# The domain is not found in the legacy system. Let Designate create it
if not resp.ok:
# We got a 404, so let Designate make the call
return self.hook_target(obj, context, domain)
# The legacy system owns the domain. Notify central it was
# an error.
#
# The `obj` is the Service object.
obj.central_api.update_status(
context, domain.id, ERROR_STATUS, domain.serial
)
It is the responsibility of the hook point author to be a good citizen and properly handle any errors / return values in the original code, as well as support any internal APIs.
Again, the intent of the Hook Point API is to allow an organization a means of injecting code, which implies a reasonably intimiate knowledge with the code.
Hook Point Management and Configuration¶
Hook points are installed by installing a package that includes designate.hook_point entry points. By default, these will be enabled and will be called when the specific hook point target is called. These hooks MAY be disabled in the config at the hook point level.
[hook_point:pool_manager_create_domain]
disabled = True
If necessary, hook points can also receive configuration details.
[hook_point:pool_manager_create_domain]
legacy_dns_api = https://my.dns.legacy.org.net:8975
The configuration will be available via the global oslo.config.cfg.CONF object.
Central Changes¶
None
Storage Changes¶
None
Other Changes¶
Hook points can be added liberally or in an extremely limited, known uses cases. Similarly, no hook points can be formally added and an organization may apply them as necessary in an org specific patch.
Alternatives¶
Other than the originally mentioned tactics for hooking into the Designate code, more specific hook points could be created on a per-use basis. For example, there could be very a specific API for hooks that get called when a message is read from the queue. While providing more specific hooks may allow for a use case specific API, it would also require each API be designed, documented and tested. The hook point API provides a single tested means of injecting code that has limited effect on the API over time and allows a reasonable level of support.
Implementation¶
Assignee(s)¶
- Primary assignee:
eric-larson
Milestones¶
- Target Milestone for completion:
Liberty-1
Work Items¶
Add designate.hookpoints, implementing the @hook_point decorator
Add documentation for writing hook points and enumrate what hook points exist.
See this review for the current implementation.
dependencies¶
stevedore