From 81cd2994a95833bb90c1c479838e8a61b0b787ad Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Fri, 5 Feb 2016 14:23:00 -0600 Subject: [PATCH] A request hook interface for the functional test client This lets us write custom hooks out of tree that are called whenever a request is made in the functional tests. The hook gets notified: 1. Just before a request, with the request args 2. Just after a request, with the resp and resp_body 3. When a request causes an exception, with the exception object (which contains the resp and resp_body if it came from tempest_lib) This is useful for maintaining certain out-of-tree test modifications, like: 1. Rate limiting client requests 2. Additional custom logging 3. ??? Change-Id: Ie2ca4ee85972aa8f9b22c402ed8fad368d2ff7d9 --- functionaltests/common/client.py | 33 ++++++++++++++++++ functionaltests/common/config.py | 2 ++ functionaltests/common/hooks/__init__.py | 37 ++++++++++++++++++++ functionaltests/common/hooks/base.py | 43 ++++++++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 functionaltests/common/hooks/__init__.py create mode 100644 functionaltests/common/hooks/base.py diff --git a/functionaltests/common/client.py b/functionaltests/common/client.py index aa9d7580a..d23f94b3e 100644 --- a/functionaltests/common/client.py +++ b/functionaltests/common/client.py @@ -15,6 +15,7 @@ limitations under the License. """ import abc +import logging from config import cfg from noauth import NoAuthAuthProvider @@ -25,6 +26,9 @@ from tempest_lib.auth import KeystoneV2Credentials from tempest_lib.auth import KeystoneV2AuthProvider from functionaltests.common.utils import memoized +from functionaltests.common import hooks + +LOG = logging.getLogger(__name__) class KeystoneV2AuthProviderWithOverridableUrl(KeystoneV2AuthProvider): @@ -59,6 +63,9 @@ class BaseDesignateClient(RestClient): if not interface.endswith('URL'): interface += "URL" + self.hooks = [] + self._populate_hooks() + super(BaseDesignateClient, self).__init__( auth_provider=self.get_auth_provider(with_token), service=cfg.CONF.designate.service, @@ -67,6 +74,32 @@ class BaseDesignateClient(RestClient): endpoint_type=interface ) + def _populate_hooks(self): + for name in cfg.CONF.testconfig.hooks: + LOG.debug("Loading request hook '%s' from config", name) + try: + cls = hooks.get_class(name) + if not cls: + LOG.debug("'%s' not found. Call register_hook", name) + else: + self.hooks.append(cls) + except Exception as e: + LOG.exception(e) + + def request(self, *args, **kwargs): + req_hooks = [hook_class() for hook_class in self.hooks] + try: + for hook in req_hooks: + hook.before(args, kwargs) + r, b = super(BaseDesignateClient, self).request(*args, **kwargs) + for hook in req_hooks: + hook.after(r, b) + return r, b + except Exception as e: + for hook in req_hooks: + hook.on_exception(e) + raise + def get_auth_provider(self, with_token=True): if cfg.CONF.noauth.use_noauth: return self._get_noauth_auth_provider() diff --git a/functionaltests/common/config.py b/functionaltests/common/config.py index 6e339c063..3e45e2e53 100644 --- a/functionaltests/common/config.py +++ b/functionaltests/common/config.py @@ -79,6 +79,8 @@ cfg.CONF.register_opts([ cfg.CONF.register_opts([ + cfg.ListOpt('hooks', default=[], + help="The list of request hook class names to enable"), cfg.StrOpt('v2_path_pattern', default='/v2/{path}', help="Specifies how to build the path for the request"), cfg.BoolOpt('no_admin_setup', default=False, diff --git a/functionaltests/common/hooks/__init__.py b/functionaltests/common/hooks/__init__.py new file mode 100644 index 000000000..aa0a9cdd4 --- /dev/null +++ b/functionaltests/common/hooks/__init__.py @@ -0,0 +1,37 @@ +""" +Copyright 2016 Rackspace + +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. +""" + +# a dictionary mapping the class name to the hook class +_HOOKS = {} + + +def register_hook(cls): + """Register the request hook. This does not enable the hook. Hooks are + enable via the config file. + + Usage: + >>> register_hook(MyHook) + """ + _HOOKS[cls.__name__] = cls + + +def get_class(name): + """Get a hook class by it's class name: + + Usage: + >>> get_hook_class('MyHook') + """ + return _HOOKS.get(name) diff --git a/functionaltests/common/hooks/base.py b/functionaltests/common/hooks/base.py new file mode 100644 index 000000000..06e48059b --- /dev/null +++ b/functionaltests/common/hooks/base.py @@ -0,0 +1,43 @@ +""" +Copyright 2016 Rackspace + +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. +""" + + +class BaseRequestHook(object): + """When writing your own hook, do three things: + + 1. Implement this hook interface + 2. Register your hook in a global lookup using hook.register_hook() + 3. Specify the name of your hook in a config file + + A new instance of a hook is created before for each request, for storing + per request state if you want. + """ + + def before(self, req_args, req_kwargs): + """A hook called before each request + + :param req_args: a list (mutable) + :param req_kwargs: a dictionary + """ + pass + + def after(self, resp, resp_body): + """A hook called after each request""" + pass + + def on_exception(self, exception): + """A hook called when an exception occurs on a request""" + pass