diff options
-rw-r--r-- | ironic/common/exception.py | 119 | ||||
-rw-r--r-- | ironic/conf/default.py | 7 | ||||
-rw-r--r-- | ironic/tests/unit/common/test_exception.py | 81 | ||||
-rw-r--r-- | ironic/tests/unit/common/test_json_rpc.py | 2 | ||||
-rw-r--r-- | lower-constraints.txt | 2 | ||||
-rw-r--r-- | releasenotes/notes/use-ironic-lib-exception-4bff237c9667bf46.yaml | 9 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | tools/config/ironic-config-generator.conf | 1 |
8 files changed, 20 insertions, 203 deletions
diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 96be2b0fa..b64640f1f 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -14,132 +14,17 @@ # License for the specific language governing permissions and limitations # under the License. -"""Ironic base exception handling. - -SHOULD include dedicated exception logging. - -""" - -import collections +"""Ironic specific exceptions list.""" +from ironic_lib.exception import IronicException from oslo_log import log as logging -from oslo_serialization import jsonutils -import six from six.moves import http_client from ironic.common.i18n import _ -from ironic.conf import CONF LOG = logging.getLogger(__name__) -def _ensure_exception_kwargs_serializable(exc_class_name, kwargs): - """Ensure that kwargs are serializable - - Ensure that all kwargs passed to exception constructor can be passed over - RPC, by trying to convert them to JSON, or, as a last resort, to string. - If it is not possible, unserializable kwargs will be removed, letting the - receiver to handle the exception string as it is configured to. - - :param exc_class_name: an IronicException class name. - :param kwargs: a dictionary of keyword arguments passed to the exception - constructor. - :returns: a dictionary of serializable keyword arguments. - """ - serializers = [(jsonutils.dumps, _('when converting to JSON')), - (six.text_type, _('when converting to string'))] - exceptions = collections.defaultdict(list) - serializable_kwargs = {} - for k, v in kwargs.items(): - for serializer, msg in serializers: - try: - serializable_kwargs[k] = serializer(v) - exceptions.pop(k, None) - break - except Exception as e: - exceptions[k].append( - '(%(serializer_type)s) %(e_type)s: %(e_contents)s' % - {'serializer_type': msg, 'e_contents': e, - 'e_type': e.__class__.__name__}) - if exceptions: - LOG.error("One or more arguments passed to the %(exc_class)s " - "constructor as kwargs can not be serialized. The " - "serialized arguments: %(serialized)s. These " - "unserialized kwargs were dropped because of the " - "exceptions encountered during their " - "serialization:\n%(errors)s", - dict(errors=';\n'.join("%s: %s" % (k, '; '.join(v)) - for k, v in exceptions.items()), - exc_class=exc_class_name, - serialized=serializable_kwargs)) - # We might be able to actually put the following keys' values into - # format string, but there is no guarantee, drop it just in case. - for k in exceptions: - del kwargs[k] - return serializable_kwargs - - -class IronicException(Exception): - """Base Ironic Exception - - To correctly use this class, inherit from it and define - a '_msg_fmt' property. That message will get printf'd - with the keyword arguments provided to the constructor. - - If you need to access the message from an exception you should use - six.text_type(exc) - - """ - _msg_fmt = _("An unknown exception occurred.") - code = http_client.INTERNAL_SERVER_ERROR - safe = False - - def __init__(self, message=None, **kwargs): - - self.kwargs = _ensure_exception_kwargs_serializable( - self.__class__.__name__, kwargs) - - if 'code' not in self.kwargs: - try: - self.kwargs['code'] = self.code - except AttributeError: - pass - else: - self.code = int(kwargs['code']) - - if not message: - try: - message = self._msg_fmt % kwargs - - except Exception as e: - # kwargs doesn't match a variable in self._msg_fmt - # log the issue and the kwargs - prs = ', '.join('%s: %s' % pair for pair in kwargs.items()) - LOG.exception('Exception in string format operation ' - '(arguments %s)', prs) - if CONF.fatal_exception_format_errors: - raise e - else: - # at least get the core self._msg_fmt out if something - # happened - message = self._msg_fmt - - super(IronicException, self).__init__(message) - - def __str__(self): - """Encode to utf-8 then wsme api can consume it as well.""" - value = self.__unicode__() - if six.PY3: - # On Python 3 unicode is the same as str - return value - else: - return value.encode('utf-8') - - def __unicode__(self): - """Return a unicode representation of the exception message.""" - return six.text_type(self.args[0]) - - class NotAuthorized(IronicException): _msg_fmt = _("Not authorized.") code = http_client.FORBIDDEN diff --git a/ironic/conf/default.py b/ironic/conf/default.py index 796ad4db7..802b8ee0a 100644 --- a/ironic/conf/default.py +++ b/ironic/conf/default.py @@ -157,10 +157,13 @@ exc_log_opts = [ help=_('Used if there is a formatting error when generating ' 'an exception message (a programming error). If True, ' 'raise an exception; if False, use the unformatted ' - 'message.')), + 'message.'), + deprecated_for_removal=True, + deprecated_reason=_('Same option in the ironic_lib section ' + 'should be used instead.')), cfg.IntOpt('log_in_db_max_size', default=4096, help=_('Max number of characters of any node ' - 'last_error/maintenance_reason pushed to database.')) + 'last_error/maintenance_reason pushed to database.')), ] hash_opts = [ diff --git a/ironic/tests/unit/common/test_exception.py b/ironic/tests/unit/common/test_exception.py deleted file mode 100644 index 5e7f6bcd6..000000000 --- a/ironic/tests/unit/common/test_exception.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2015 IBM, Corp. -# -# 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 - -import mock -import six - -from ironic.common import exception -from ironic.tests import base - - -class Unserializable(object): - - def __str__(self): - raise NotImplementedError('nostr') - - -class TestException(exception.IronicException): - _msg_fmt = 'Some exception: %(spam)s, %(ham)s' - - -class TestIronicException(base.TestCase): - def test___str__encoding(self): - expected = b'\xc3\xa9\xe0\xaf\xb2\xe0\xbe\x84' - if six.PY3: - expected = expected.decode('utf-8') - message = six.unichr(233) + six.unichr(0x0bf2) + six.unichr(3972) - exc = exception.IronicException(message) - self.assertEqual(expected, exc.__str__()) - - def test___str__non_string(self): - exc = exception.IronicException(42) - self.assertEqual("42", exc.__str__()) - self.assertEqual(u"42", exc.__unicode__()) - - @mock.patch.object(exception.LOG, 'error', autospec=True) - def test___init___invalid_kwarg(self, log_mock): - self.config(fatal_exception_format_errors=False) - e = TestException(spam=Unserializable(), ham='eggs') - message = log_mock.call_args[0][0] % log_mock.call_args[0][1] - self.assertIsNotNone( - re.search('spam: .*JSON.* ValueError: Circular reference detected;' - '.*string.* NotImplementedError: nostr', message) - ) - self.assertEqual({'ham': '"eggs"', 'code': 500}, e.kwargs) - - @mock.patch.object(exception.LOG, 'error', autospec=True) - def test___init___invalid_kwarg_reraise(self, log_mock): - self.config(fatal_exception_format_errors=True) - self.assertRaises(KeyError, TestException, spam=Unserializable(), - ham='eggs') - message = log_mock.call_args[0][0] % log_mock.call_args[0][1] - self.assertIsNotNone( - re.search('spam: .*JSON.* ValueError: Circular reference detected;' - '.*string.* NotImplementedError: nostr', message) - ) - - def test___init___json_serializable(self): - exc = TestException(spam=[1, 2, 3], ham='eggs') - self.assertIn('[1, 2, 3]', six.text_type(exc)) - self.assertEqual('[1, 2, 3]', exc.kwargs['spam']) - - def test___init___string_serializable(self): - exc = TestException( - spam=type('ni', (object,), dict(a=1, b=2))(), ham='eggs' - ) - check_str = 'ni object at' - self.assertIn(check_str, six.text_type(exc)) - self.assertIn(check_str, exc.kwargs['spam']) diff --git a/ironic/tests/unit/common/test_json_rpc.py b/ironic/tests/unit/common/test_json_rpc.py index 082eaa0a0..a5d36e0d7 100644 --- a/ironic/tests/unit/common/test_json_rpc.py +++ b/ironic/tests/unit/common/test_json_rpc.py @@ -199,7 +199,7 @@ class TestService(test_base.TestCase): 'message': 'some error', 'code': 500, 'data': { - 'class': 'ironic.common.exception.IronicException' + 'class': 'ironic_lib.exception.IronicException' } }) diff --git a/lower-constraints.txt b/lower-constraints.txt index 859c21044..5271595d0 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ greenlet==0.4.13 hacking==1.0.0 idna==2.6 imagesize==1.0.0 -ironic-lib==2.17.0 +ironic-lib==2.17.1 iso8601==0.1.11 Jinja2==2.10 jmespath==0.9.3 diff --git a/releasenotes/notes/use-ironic-lib-exception-4bff237c9667bf46.yaml b/releasenotes/notes/use-ironic-lib-exception-4bff237c9667bf46.yaml new file mode 100644 index 000000000..9a6f8c2ea --- /dev/null +++ b/releasenotes/notes/use-ironic-lib-exception-4bff237c9667bf46.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - The configuration option ``[DEFAULT]/fatal_exception_format_errors`` is now + deprecated. + Please use the configuration option + ``[ironic_lib]/fatal_exception_format_errors`` instead. +other: + - Use the module exception from ironic-lib. + Update ironic-lib minimum version required to 2.17.1. diff --git a/requirements.txt b/requirements.txt index 868e64962..86c36219d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 -ironic-lib>=2.17.0 # Apache-2.0 +ironic-lib>=2.17.1 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0 pytz>=2013.6 # MIT stevedore>=1.20.0 # Apache-2.0 diff --git a/tools/config/ironic-config-generator.conf b/tools/config/ironic-config-generator.conf index 806bf7a03..ca5feb58f 100644 --- a/tools/config/ironic-config-generator.conf +++ b/tools/config/ironic-config-generator.conf @@ -4,6 +4,7 @@ wrap_width = 62 namespace = ironic namespace = ironic_lib.disk_utils namespace = ironic_lib.disk_partitioner +namespace = ironic_lib.exception namespace = ironic_lib.mdns namespace = ironic_lib.metrics namespace = ironic_lib.metrics_statsd |