diff --git a/oslo/messaging/_drivers/common.py b/oslo/messaging/_drivers/common.py index 93316c9d7..4e25d1f0f 100644 --- a/oslo/messaging/_drivers/common.py +++ b/oslo/messaging/_drivers/common.py @@ -72,6 +72,18 @@ _MESSAGE_KEY = 'oslo.message' _REMOTE_POSTFIX = '_Remote' +# FIXME(markmc): add an API to replace this option +_exception_opts = [ + cfg.ListOpt('allowed_rpc_exception_modules', + default=['oslo.messaging.exceptions', + 'nova.exception', + 'cinder.exception', + 'exceptions', + ], + help='Modules of exceptions that are permitted to be recreated' + 'upon receiving exception data from an rpc call.'), +] + class RPCException(Exception): msg_fmt = _("An unknown RPC related exception occurred.") @@ -327,6 +339,7 @@ def deserialize_remote_exception(conf, data): # NOTE(ameade): We DO NOT want to allow just any module to be imported, in # order to prevent arbitrary code execution. + conf.register_opts(_exception_opts) if module not in conf.allowed_rpc_exception_modules: return RemoteError(name, failure.get('message'), trace) diff --git a/tests/test_exception_serialization.py b/tests/test_exception_serialization.py new file mode 100644 index 000000000..c3103a22b --- /dev/null +++ b/tests/test_exception_serialization.py @@ -0,0 +1,314 @@ + +# Copyright 2013 Red Hat, Inc. +# +# 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 testscenarios + +from oslo.messaging._drivers import common as exceptions +from oslo.messaging.openstack.common import jsonutils +from tests import utils as test_utils + +load_tests = testscenarios.load_tests_apply_scenarios + + +class NovaStyleException(Exception): + + format = 'I am Nova' + + def __init__(self, message=None, **kwargs): + self.kwargs = kwargs + if not message: + message = self.format % kwargs + super(NovaStyleException, self).__init__(message) + + +class KwargsStyleException(NovaStyleException): + + format = 'I am %(who)s' + + +def add_remote_postfix(ex): + ex_type = type(ex) + message = str(ex) + str_override = lambda self: message + new_ex_type = type(ex_type.__name__ + "_Remote", (ex_type,), + {'__str__': str_override, + '__unicode__': str_override}) + new_ex_type.__module__ = '%s_Remote' % ex.__class__.__module__ + try: + ex.__class__ = new_ex_type + except TypeError: + ex.args = (message,) + ex.args[1:] + return ex + + +class SerializeRemoteExceptionTestCase(test_utils.BaseTestCase): + + _log_failure = [ + ('log_failure', dict(log_failure=True)), + ('do_not_log_failure', dict(log_failure=False)), + ] + + _add_remote = [ + ('add_remote', dict(add_remote=True)), + ('do_not_add_remote', dict(add_remote=False)), + ] + + _exception_types = [ + ('bog_standard', dict(cls=Exception, + args=['test'], + kwargs={}, + clsname='Exception', + modname='exceptions', + msg='test')), + ('nova_style', dict(cls=NovaStyleException, + args=[], + kwargs={}, + clsname='NovaStyleException', + modname=__name__, + msg='I am Nova')), + ('nova_style_with_msg', dict(cls=NovaStyleException, + args=['testing'], + kwargs={}, + clsname='NovaStyleException', + modname=__name__, + msg='testing')), + ('kwargs_style', dict(cls=KwargsStyleException, + args=[], + kwargs={'who': 'Oslo'}, + clsname='KwargsStyleException', + modname=__name__, + msg='I am Oslo')), + ] + + @classmethod + def generate_scenarios(cls): + cls.scenarios = testscenarios.multiply_scenarios(cls._log_failure, + cls._add_remote, + cls._exception_types) + + def setUp(self): + super(SerializeRemoteExceptionTestCase, self).setUp() + + def test_serialize_remote_exception(self): + errors = [] + + def stub_error(msg, *a, **kw): + if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]): + a = a[0] + errors.append(str(msg) % a) + + self.stubs.Set(exceptions.LOG, 'error', stub_error) + + try: + try: + raise self.cls(*self.args, **self.kwargs) + except Exception as ex: + if self.add_remote: + ex = add_remote_postfix(ex) + raise ex + except Exception: + exc_info = sys.exc_info() + + serialized = exceptions.serialize_remote_exception( + exc_info, log_failure=self.log_failure) + + failure = jsonutils.loads(serialized) + + self.assertEqual(failure['class'], self.clsname, failure) + self.assertEqual(failure['module'], self.modname) + self.assertEqual(failure['message'], self.msg) + self.assertEqual(failure['args'], [self.msg]) + self.assertEqual(failure['kwargs'], self.kwargs) + + # Note: _Remote prefix not stripped from tracebacks + tb = ex.__class__.__name__ + ': ' + self.msg + self.assertTrue(tb in ''.join(failure['tb'])) + + if self.log_failure: + self.assertTrue(len(errors) > 0, errors) + else: + self.assertEqual(len(errors), 0, errors) + + +SerializeRemoteExceptionTestCase.generate_scenarios() + + +class DeserializeRemoteExceptionTestCase(test_utils.BaseTestCase): + + _standard_allowed = [__name__, 'exceptions'] + + scenarios = [ + ('bog_standard', + dict(allowed=_standard_allowed, + clsname='Exception', + modname='exceptions', + cls=Exception, + args=['test'], + kwargs={}, + str='test\ntraceback\ntraceback\n', + msg='test', + remote_name='Exception', + remote_args=('test\ntraceback\ntraceback\n', ), + remote_kwargs={})), + ('nova_style', + dict(allowed=_standard_allowed, + clsname='NovaStyleException', + modname=__name__, + cls=NovaStyleException, + args=[], + kwargs={}, + str='test\ntraceback\ntraceback\n', + msg='I am Nova', + remote_name='NovaStyleException_Remote', + remote_args=('I am Nova', ), + remote_kwargs={})), + ('nova_style_with_msg', + dict(allowed=_standard_allowed, + clsname='NovaStyleException', + modname=__name__, + cls=NovaStyleException, + args=['testing'], + kwargs={}, + str='test\ntraceback\ntraceback\n', + msg='testing', + remote_name='NovaStyleException_Remote', + remote_args=('testing', ), + remote_kwargs={})), + ('kwargs_style', + dict(allowed=_standard_allowed, + clsname='KwargsStyleException', + modname=__name__, + cls=KwargsStyleException, + args=[], + kwargs={'who': 'Oslo'}, + str='test\ntraceback\ntraceback\n', + msg='I am Oslo', + remote_name='KwargsStyleException_Remote', + remote_args=('I am Oslo', ), + remote_kwargs={})), + ('not_allowed', + dict(allowed=[], + clsname='Exception', + modname='exceptions', + cls=exceptions.RemoteError, + args=[], + kwargs={}, + str=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + msg=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + remote_name='RemoteError', + remote_args=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n'].", ), + remote_kwargs={'exc_type': 'Exception', + 'value': 'test', + 'traceback': 'traceback\ntraceback\n'})), + ('unknown_module', + dict(allowed=['notexist'], + clsname='Exception', + modname='notexist', + cls=exceptions.RemoteError, + args=[], + kwargs={}, + str=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + msg=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + remote_name='RemoteError', + remote_args=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n'].", ), + remote_kwargs={'exc_type': 'Exception', + 'value': 'test', + 'traceback': 'traceback\ntraceback\n'})), + ('unknown_exception', + dict(allowed=['exceptions'], + clsname='FarcicalError', + modname='exceptions', + cls=exceptions.RemoteError, + args=[], + kwargs={}, + str=("Remote error: FarcicalError test\n" + "[u'traceback\\ntraceback\\n']."), + msg=("Remote error: FarcicalError test\n" + "[u'traceback\\ntraceback\\n']."), + remote_name='RemoteError', + remote_args=("Remote error: FarcicalError test\n" + "[u'traceback\\ntraceback\\n'].", ), + remote_kwargs={'exc_type': 'FarcicalError', + 'value': 'test', + 'traceback': 'traceback\ntraceback\n'})), + ('unknown_kwarg', + dict(allowed=['exceptions'], + clsname='Exception', + modname='exceptions', + cls=exceptions.RemoteError, + args=[], + kwargs={'foobar': 'blaa'}, + str=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + msg=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n']."), + remote_name='RemoteError', + remote_args=("Remote error: Exception test\n" + "[u'traceback\\ntraceback\\n'].", ), + remote_kwargs={'exc_type': 'Exception', + 'value': 'test', + 'traceback': 'traceback\ntraceback\n'})), + ('system_exit', + dict(allowed=['exceptions'], + clsname='SystemExit', + modname='exceptions', + cls=exceptions.RemoteError, + args=[], + kwargs={}, + str=("Remote error: SystemExit test\n" + "[u'traceback\\ntraceback\\n']."), + msg=("Remote error: SystemExit test\n" + "[u'traceback\\ntraceback\\n']."), + remote_name='RemoteError', + remote_args=("Remote error: SystemExit test\n" + "[u'traceback\\ntraceback\\n'].", ), + remote_kwargs={'exc_type': 'SystemExit', + 'value': 'test', + 'traceback': 'traceback\ntraceback\n'})), + ] + + def setUp(self): + super(DeserializeRemoteExceptionTestCase, self).setUp() + self.conf.register_opts(exceptions._exception_opts) + + def test_deserialize_remote_exception(self): + self.config(allowed_rpc_exception_modules=self.allowed) + + failure = { + 'class': self.clsname, + 'module': self.modname, + 'message': 'test', + 'tb': ['traceback\ntraceback\n'], + 'args': self.args, + 'kwargs': self.kwargs, + } + + serialized = jsonutils.dumps(failure) + + ex = exceptions.deserialize_remote_exception(self.conf, serialized) + + self.assertTrue(isinstance(ex, self.cls)) + self.assertEqual(ex.__class__.__name__, self.remote_name) + self.assertEqual(str(ex), self.str) + self.assertEqual(ex.message, self.msg) + self.assertEqual(ex.args, self.remote_args)