diff --git a/horizon/conf/__init__.py b/horizon/conf/__init__.py index 977d6f4431..73a3af5c4e 100644 --- a/horizon/conf/__init__.py +++ b/horizon/conf/__init__.py @@ -23,6 +23,12 @@ class LazySettings(LazyObject): HORIZON_CONFIG = copy.copy(DEFAULT_CONFIG) HORIZON_CONFIG.update(settings.HORIZON_CONFIG) + # Ensure we always have our exception configuration... + for exc_category in ['unauthorized', 'not_found', 'recoverable']: + if exc_category not in HORIZON_CONFIG['exceptions']: + default_exc_config = DEFAULT_CONFIG['exceptions'][exc_category] + HORIZON_CONFIG['exceptions'][exc_category] = default_exc_config + # Ensure our password validator always exists... if 'regex' not in HORIZON_CONFIG['password_validator']: default_pw_regex = DEFAULT_CONFIG['password_validator']['regex'] diff --git a/horizon/conf/default.py b/horizon/conf/default.py index 27d755f673..7ad8da2506 100644 --- a/horizon/conf/default.py +++ b/horizon/conf/default.py @@ -31,6 +31,11 @@ HORIZON_CONFIG = { # URL for additional help with this site. 'help_url': None, + # Exception configuration. + 'exceptions': {'unauthorized': [], + 'not_found': [], + 'recoverable': []}, + # Password configuration. 'password_validator': {'regex': '.*', 'help_text': _("Password is not accepted")}, diff --git a/horizon/exceptions.py b/horizon/exceptions.py index ea0509999e..f6ec76505b 100644 --- a/horizon/exceptions.py +++ b/horizon/exceptions.py @@ -22,7 +22,6 @@ import sys import six -from django.conf import settings from django.core.management import color_style # noqa from django.http import HttpRequest # noqa from django.utils import encoding @@ -30,6 +29,7 @@ from django.utils.translation import ugettext_lazy as _ from django.views.debug import CLEANSED_SUBSTITUTE # noqa from django.views.debug import SafeExceptionReporterFilter # noqa +from horizon.conf import HORIZON_CONFIG # noqa from horizon import messages LOG = logging.getLogger(__name__) @@ -202,6 +202,12 @@ class HandledException(HorizonException): self.wrapped = wrapped +UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized']) +NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found']) +RECOVERABLE = (AlreadyExists, Conflict, NotAvailable, ServiceCatalogException) +RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable']) + + def error_color(msg): return color_style().ERROR_OUTPUT(msg) @@ -274,6 +280,13 @@ def handle_recoverable(request, message, redirect, ignore, escalate, handled, return RecoverableError # return to normal code flow +HANDLE_EXC_METHODS = [ + {'exc': UNAUTHORIZED, 'handler': handle_unauthorized, 'set_wrap': False}, + {'exc': NOT_FOUND, 'handler': handle_notfound, 'set_wrap': True}, + {'exc': RECOVERABLE, 'handler': handle_recoverable, 'set_wrap': True}, +] + + def handle(request, message=None, redirect=None, ignore=False, escalate=False, log_level=None, force_log=None): """Centralized error handling for Horizon. @@ -303,19 +316,6 @@ def handle(request, message=None, redirect=None, ignore=False, class indicating the type of exception that was encountered will be returned. """ - HORIZON_CONFIG = settings.HORIZON_CONFIG - UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized']) - NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found']) - RECOVERABLE = (AlreadyExists, Conflict, NotAvailable, - ServiceCatalogException) - RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable']) - HANDLE_EXC_METHODS = [ - {'exc': UNAUTHORIZED, 'handler': handle_unauthorized, - 'set_wrap': False}, - {'exc': NOT_FOUND, 'handler': handle_notfound, 'set_wrap': True}, - {'exc': RECOVERABLE, 'handler': handle_recoverable, 'set_wrap': True} - ] - exc_type, exc_value, exc_traceback = sys.exc_info() log_method = getattr(LOG, log_level or "exception") force_log = force_log or os.environ.get("HORIZON_TEST_RUN", False) diff --git a/horizon/test/settings.py b/horizon/test/settings.py index 80cb22839d..f7bc828d77 100644 --- a/horizon/test/settings.py +++ b/horizon/test/settings.py @@ -20,17 +20,6 @@ import os import socket import sys -from cinderclient import exceptions as cinderclient -from glanceclient.common import exceptions as glanceclient -from heatclient import exc as heatclient -from keystoneclient import exceptions as keystoneclient -from neutronclient.common import exceptions as neutronclient -from novaclient import exceptions as novaclient -from requests import exceptions as requests -from saharaclient.api import base as saharaclient -from swiftclient import client as swiftclient -from troveclient import exceptions as troveclient - import django from django.utils import html_parser from openstack_dashboard.static_settings import get_staticfiles_dirs # noqa @@ -146,37 +135,6 @@ HORIZON_CONFIG = { }, 'user_home': None, 'help_url': "http://example.com", - 'exceptions': {'recoverable': (keystoneclient.ClientException, - keystoneclient.AuthorizationFailure, - keystoneclient.Forbidden, - cinderclient.ClientException, - cinderclient.ConnectionError, - cinderclient.Forbidden, - novaclient.ClientException, - novaclient.Forbidden, - glanceclient.ClientException, - neutronclient.Forbidden, - neutronclient.NeutronClientException, - swiftclient.ClientException, - heatclient.HTTPForbidden, - heatclient.HTTPException, - troveclient.ClientException, - saharaclient.APIException, - requests.RequestException), - 'not_found': (keystoneclient.NotFound, - cinderclient.NotFound, - novaclient.NotFound, - glanceclient.NotFound, - neutronclient.NotFound, - heatclient.HTTPNotFound, - troveclient.NotFound), - 'unauthorized': (keystoneclient.Unauthorized, - cinderclient.Unauthorized, - novaclient.Unauthorized, - glanceclient.Unauthorized, - neutronclient.Unauthorized, - heatclient.HTTPUnauthorized, - troveclient.Unauthorized)} } COMPRESS_ENABLED = True diff --git a/openstack_dashboard/api/rest/utils.py b/openstack_dashboard/api/rest/utils.py index 549c4b7d6b..da7a720187 100644 --- a/openstack_dashboard/api/rest/utils.py +++ b/openstack_dashboard/api/rest/utils.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import functools -import itertools import json import logging @@ -22,6 +21,7 @@ from django.utils import decorators from oslo_serialization import jsonutils +from horizon import exceptions log = logging.getLogger(__name__) @@ -31,6 +31,9 @@ class AjaxError(Exception): self.http_status = http_status super(AjaxError, self).__init__(msg) +http_errors = exceptions.UNAUTHORIZED + exceptions.NOT_FOUND + \ + exceptions.RECOVERABLE + (AjaxError, ) + class CreatedResponse(http.HttpResponse): def __init__(self, location, data=None): @@ -107,8 +110,6 @@ def ajax(authenticated=True, data_required=False): return JSONResponse('request requires JSON body', 400) # invoke the wrapped function, handling exceptions sanely - horizon_exc = settings.HORIZON_CONFIG['exceptions'].values() - api_exc = itertools.chain([AjaxError, ], *horizon_exc) try: data = function(self, request, *args, **kw) if isinstance(data, http.HttpResponse): @@ -116,15 +117,18 @@ def ajax(authenticated=True, data_required=False): elif data is None: return JSONResponse('', status=204) return JSONResponse(data) - except tuple(api_exc) as e: + except http_errors as e: + # exception was raised with a specific HTTP status if hasattr(e, 'http_status'): http_status = e.http_status + elif hasattr(e, 'code'): + http_status = e.code else: - http_status = getattr(e, 'code', 500) - log.exception('API Error: %s', e) + log.exception('HTTP exception with no status/code') + return JSONResponse(str(e), 500) return JSONResponse(str(e), http_status) except Exception as e: - log.exception('Internal Error: %s', e) + log.exception('error invoking apiclient') return JSONResponse(str(e), 500) return _wrapped diff --git a/openstack_dashboard/exceptions.py b/openstack_dashboard/exceptions.py new file mode 100644 index 0000000000..a84d09f6a4 --- /dev/null +++ b/openstack_dashboard/exceptions.py @@ -0,0 +1,73 @@ +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, 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. + +from cinderclient import exceptions as cinderclient +from glanceclient.common import exceptions as glanceclient +from heatclient import exc as heatclient +from keystoneclient import exceptions as keystoneclient +from neutronclient.common import exceptions as neutronclient +from novaclient import exceptions as novaclient +from requests import exceptions as requests +from saharaclient.api import base as saharaclient +from swiftclient import client as swiftclient +from troveclient import exceptions as troveclient + + +UNAUTHORIZED = ( + keystoneclient.Unauthorized, + cinderclient.Unauthorized, + novaclient.Unauthorized, + glanceclient.Unauthorized, + neutronclient.Unauthorized, + heatclient.HTTPUnauthorized, + troveclient.Unauthorized, +) + + +NOT_FOUND = ( + keystoneclient.NotFound, + cinderclient.NotFound, + novaclient.NotFound, + glanceclient.NotFound, + neutronclient.NotFound, + heatclient.HTTPNotFound, + troveclient.NotFound, +) + + +# NOTE(gabriel): This is very broad, and may need to be dialed in. +RECOVERABLE = ( + keystoneclient.ClientException, + # AuthorizationFailure is raised when Keystone is "unavailable". + keystoneclient.AuthorizationFailure, + keystoneclient.Forbidden, + cinderclient.ClientException, + cinderclient.ConnectionError, + cinderclient.Forbidden, + novaclient.ClientException, + novaclient.Forbidden, + glanceclient.ClientException, + neutronclient.Forbidden, + neutronclient.NeutronClientException, + swiftclient.ClientException, + heatclient.HTTPForbidden, + heatclient.HTTPException, + troveclient.ClientException, + saharaclient.APIException, + requests.RequestException, +) diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 8bc7e4bdd3..16c60af0a3 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -2,6 +2,7 @@ import os from django.utils.translation import ugettext_lazy as _ +from openstack_dashboard import exceptions DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -73,9 +74,9 @@ HORIZON_CONFIG = { 'types': ['alert-success', 'alert-info'] }, 'help_url': "http://docs.openstack.org", - 'exceptions': {'recoverable': [], - 'not_found': [], - 'unauthorized': []}, + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, 'modal_backdrop': 'static', 'angular_modules': [], 'js_files': [], diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 2c5cefa987..478bfcf7a0 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -21,20 +21,10 @@ import os import sys import warnings -from cinderclient import exceptions as cinderclient -from glanceclient.common import exceptions as glanceclient -from heatclient import exc as heatclient -from keystoneclient import exceptions as keystoneclient -from neutronclient.common import exceptions as neutronclient -from novaclient import exceptions as novaclient -from requests import exceptions as requests -from saharaclient.api import base as saharaclient -from swiftclient import client as swiftclient -from troveclient import exceptions as troveclient - import django from django.utils.translation import ugettext_lazy as _ +from openstack_dashboard import exceptions from openstack_dashboard.static_settings import get_staticfiles_dirs # noqa @@ -69,37 +59,9 @@ HORIZON_CONFIG = { 'types': ['alert-success', 'alert-info'] }, 'help_url': "http://docs.openstack.org", - 'exceptions': {'recoverable': (keystoneclient.ClientException, - keystoneclient.AuthorizationFailure, - keystoneclient.Forbidden, - cinderclient.ClientException, - cinderclient.ConnectionError, - cinderclient.Forbidden, - novaclient.ClientException, - novaclient.Forbidden, - glanceclient.ClientException, - neutronclient.Forbidden, - neutronclient.NeutronClientException, - swiftclient.ClientException, - heatclient.HTTPForbidden, - heatclient.HTTPException, - troveclient.ClientException, - saharaclient.APIException, - requests.RequestException), - 'not_found': (keystoneclient.NotFound, - cinderclient.NotFound, - novaclient.NotFound, - glanceclient.NotFound, - neutronclient.NotFound, - heatclient.HTTPNotFound, - troveclient.NotFound), - 'unauthorized': (keystoneclient.Unauthorized, - cinderclient.Unauthorized, - novaclient.Unauthorized, - glanceclient.Unauthorized, - neutronclient.Unauthorized, - heatclient.HTTPUnauthorized, - troveclient.Unauthorized)}, + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, 'angular_modules': [], 'js_files': [], 'js_spec_files': [], diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 64ebcff660..db01994c6b 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -12,19 +12,9 @@ import os -from cinderclient import exceptions as cinderclient -from glanceclient.common import exceptions as glanceclient -from heatclient import exc as heatclient -from keystoneclient import exceptions as keystoneclient -from neutronclient.common import exceptions as neutronclient -from novaclient import exceptions as novaclient -from requests import exceptions as requests -from saharaclient.api import base as saharaclient -from swiftclient import client as swiftclient -from troveclient import exceptions as troveclient - from horizon.test.settings import * # noqa from horizon.utils import secret_key +from openstack_dashboard import exceptions from openstack_dashboard.static_settings import get_staticfiles_dirs # noqa STATICFILES_DIRS = get_staticfiles_dirs() @@ -76,39 +66,11 @@ HORIZON_CONFIG = { }, 'user_home': None, 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, 'angular_modules': [], 'js_files': [], - 'exceptions': {'recoverable': (keystoneclient.ClientException, - keystoneclient.AuthorizationFailure, - keystoneclient.Forbidden, - cinderclient.ClientException, - cinderclient.ConnectionError, - cinderclient.Forbidden, - novaclient.ClientException, - novaclient.Forbidden, - glanceclient.ClientException, - neutronclient.Forbidden, - neutronclient.NeutronClientException, - swiftclient.ClientException, - heatclient.HTTPForbidden, - heatclient.HTTPException, - troveclient.ClientException, - saharaclient.APIException, - requests.RequestException), - 'not_found': (keystoneclient.NotFound, - cinderclient.NotFound, - novaclient.NotFound, - glanceclient.NotFound, - neutronclient.NotFound, - heatclient.HTTPNotFound, - troveclient.NotFound), - 'unauthorized': (keystoneclient.Unauthorized, - cinderclient.Unauthorized, - novaclient.Unauthorized, - glanceclient.Unauthorized, - neutronclient.Unauthorized, - heatclient.HTTPUnauthorized, - troveclient.Unauthorized)}, } # Set to True to allow users to upload images to glance via Horizon server.