deb-horizon/horizon/exceptions.py
Sam Betts 5ca1a52300 Ensure exc message is included in user_message
In exception.handle the user_message was being overridden so
failed to include the exception message even if %(exc)s was in
the message. This patch ensures the correct user_message is
produced by exception.handle()

Change-Id: I2bcc041a2f8a3b929c1acbbf3d3ff1e76b5646e9
Closes-Bug: #1493025
2015-09-07 14:32:02 +01:00

373 lines
14 KiB
Python

# 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.
"""
Exceptions raised by the Horizon code and the machinery for handling them.
"""
import logging
import os
import sys
import six
from django.core.management import color_style # noqa
from django.http import HttpRequest # noqa
from django.utils import encoding
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__)
class HorizonReporterFilter(SafeExceptionReporterFilter):
"""Error report filter that's always active, even in DEBUG mode."""
def is_active(self, request):
return True
# TODO(gabriel): This bugfix is cribbed from Django's code. When 1.4.1
# is available we can remove this code.
def get_traceback_frame_variables(self, request, tb_frame):
"""Replaces the values of variables marked as sensitive with
stars (*********).
"""
# Loop through the frame's callers to see if the sensitive_variables
# decorator was used.
current_frame = tb_frame.f_back
sensitive_variables = None
while current_frame is not None:
if (current_frame.f_code.co_name == 'sensitive_variables_wrapper'
and 'sensitive_variables_wrapper'
in current_frame.f_locals):
# The sensitive_variables decorator was used, so we take note
# of the sensitive variables' names.
wrapper = current_frame.f_locals['sensitive_variables_wrapper']
sensitive_variables = getattr(wrapper,
'sensitive_variables',
None)
break
current_frame = current_frame.f_back
cleansed = []
if self.is_active(request) and sensitive_variables:
if sensitive_variables == '__ALL__':
# Cleanse all variables
for name, value in tb_frame.f_locals.items():
cleansed.append((name, CLEANSED_SUBSTITUTE))
return cleansed
else:
# Cleanse specified variables
for name, value in tb_frame.f_locals.items():
if name in sensitive_variables:
value = CLEANSED_SUBSTITUTE
elif isinstance(value, HttpRequest):
# Cleanse the request's POST parameters.
value = self.get_request_repr(value)
cleansed.append((name, value))
return cleansed
else:
# Potentially cleanse only the request if it's one of the
# frame variables.
for name, value in tb_frame.f_locals.items():
if isinstance(value, HttpRequest):
# Cleanse the request's POST parameters.
value = self.get_request_repr(value)
cleansed.append((name, value))
return cleansed
class HorizonException(Exception):
"""Base exception class for distinguishing our own exception classes."""
pass
class Http302(HorizonException):
"""Error class which can be raised from within a handler to cause an
early bailout and redirect at the middleware level.
"""
status_code = 302
def __init__(self, location, message=None):
self.location = location
self.message = message
class NotAuthorized(HorizonException):
"""Raised whenever a user attempts to access a resource which they do not
have permission-based access to (such as when failing the
:func:`~horizon.decorators.require_perms` decorator).
The included :class:`~horizon.middleware.HorizonMiddleware` catches
``NotAuthorized`` and handles it gracefully by displaying an error
message and redirecting the user to a login page.
"""
status_code = 401
class NotAuthenticated(HorizonException):
"""Raised when a user is trying to make requests and they are not logged
in.
The included :class:`~horizon.middleware.HorizonMiddleware` catches
``NotAuthenticated`` and handles it gracefully by displaying an error
message and redirecting the user to a login page.
"""
status_code = 403
class NotFound(HorizonException):
"""Generic error to replace all "Not Found"-type API errors."""
status_code = 404
class Conflict(HorizonException):
"""Generic error to replace all "Conflict"-type API errors."""
status_code = 409
class RecoverableError(HorizonException):
"""Generic error to replace any "Recoverable"-type API errors."""
status_code = 100 # HTTP status code "Continue"
class ServiceCatalogException(HorizonException):
"""Raised when a requested service is not available in the
``ServiceCatalog`` returned by Keystone.
"""
def __init__(self, service_name):
message = 'Invalid service catalog service: %s' % service_name
super(ServiceCatalogException, self).__init__(message)
@six.python_2_unicode_compatible
class AlreadyExists(HorizonException):
"""Exception to be raised when trying to create an API resource which
already exists.
"""
def __init__(self, name, resource_type):
self.attrs = {"name": name, "resource": resource_type}
self.msg = _('A %(resource)s with the name "%(name)s" already exists.')
def __repr__(self):
return self.msg % self.attrs
def __str__(self):
return self.msg % self.attrs
class ConfigurationError(HorizonException):
"""Exception to be raised when invalid settings have been provided."""
pass
class NotAvailable(HorizonException):
"""Exception to be raised when something is not available."""
pass
class WorkflowError(HorizonException):
"""Exception to be raised when something goes wrong in a workflow."""
pass
class WorkflowValidationError(HorizonException):
"""Exception raised during workflow validation if required data is missing,
or existing data is not valid.
"""
pass
class HandledException(HorizonException):
"""Used internally to track exceptions that have gone through
:func:`horizon.exceptions.handle` more than once.
"""
def __init__(self, wrapped):
self.wrapped = wrapped
UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized'])
UNAUTHORIZED += (NotAuthorized,)
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)
def check_message(keywords, message):
"""Checks an exception for given keywords and raises a new ``ActionError``
with the desired message if the keywords are found. This allows selective
control over API error messages.
"""
exc_type, exc_value, exc_traceback = sys.exc_info()
if set(str(exc_value).split(" ")).issuperset(set(keywords)):
exc_value.message = message
raise
def handle_unauthorized(request, message, redirect, ignore, escalate, handled,
force_silence, force_log,
log_method, log_entry, log_level):
if ignore:
return NotAuthorized
if not force_silence and not handled:
log_method(error_color("Unauthorized: %s" % log_entry))
if not handled:
if message:
message = _("Unauthorized: %s") % message
# We get some pretty useless error messages back from
# some clients, so let's define our own fallback.
fallback = _("Unauthorized. Please try logging in again.")
messages.error(request, message or fallback)
# Escalation means logging the user out and raising NotAuthorized
# so the middleware will redirect them appropriately.
if escalate:
# Prevents creation of circular import. django.contrib.auth
# requires openstack_dashboard.settings to be loaded (by trying to
# access settings.CACHES in in django.core.caches) while
# openstack_dashboard.settings requires django.contrib.auth to be
# loaded while importing openstack_auth.utils
from django.contrib.auth import logout # noqa
logout(request)
raise NotAuthorized
# Otherwise continue and present our "unauthorized" error message.
return NotAuthorized
def handle_notfound(request, message, redirect, ignore, escalate, handled,
force_silence, force_log,
log_method, log_entry, log_level):
if not force_silence and not handled and (not ignore or force_log):
log_method(error_color("Not Found: %s" % log_entry))
if not ignore and not handled:
messages.error(request, message or log_entry)
if redirect:
raise Http302(redirect)
if not escalate:
return NotFound # return to normal code flow
def handle_recoverable(request, message, redirect, ignore, escalate, handled,
force_silence, force_log,
log_method, log_entry, log_level):
if not force_silence and not handled and (not ignore or force_log):
# Default recoverable error to WARN log level
log_method = getattr(LOG, log_level or "warning")
log_method(error_color("Recoverable error: %s" % log_entry))
if not ignore and not handled:
messages.error(request, message or log_entry)
if redirect:
raise Http302(redirect)
if not escalate:
return RecoverableError # return to normal code flow
HANDLE_EXC_METHODS = [
{'exc': UNAUTHORIZED, 'handler': handle_unauthorized,
'set_wrap': False, 'escalate': True},
{'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.
Because Horizon consumes so many different APIs with completely
different ``Exception`` types, it's necessary to have a centralized
place for handling exceptions which may be raised.
Exceptions are roughly divided into 3 types:
#. ``UNAUTHORIZED``: Errors resulting from authentication or authorization
problems. These result in being logged out and sent to the login screen.
#. ``NOT_FOUND``: Errors resulting from objects which could not be
located via the API. These generally result in a user-facing error
message, but are otherwise returned to the normal code flow. Optionally
a redirect value may be passed to the error handler so users are
returned to a different view than the one requested in addition to the
error message.
#. RECOVERABLE: Generic API errors which generate a user-facing message
but drop directly back to the regular code flow.
All other exceptions bubble the stack as normal unless the ``ignore``
argument is passed in as ``True``, in which case only unrecognized
errors are bubbled.
If the exception is not re-raised, an appropriate wrapper exception
class indicating the type of exception that was encountered will be
returned.
"""
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)
force_silence = getattr(exc_value, "silence_logging", False)
# Because the same exception may travel through this method more than
# once (if it's re-raised) we may want to treat it differently
# the second time (e.g. no user messages/logging).
handled = issubclass(exc_type, HandledException)
wrap = False
# Restore our original exception information, but re-wrap it at the end
if handled:
exc_type, exc_value, exc_traceback = exc_value.wrapped
wrap = True
log_entry = encoding.force_text(exc_value)
user_message = ""
# We trust messages from our own exceptions
if issubclass(exc_type, HorizonException):
user_message = log_entry
# If the message has a placeholder for the exception, fill it in
elif message and "%(exc)s" in message:
user_message = encoding.force_text(message) % {"exc": log_entry}
elif message:
user_message = encoding.force_text(message)
for exc_handler in HANDLE_EXC_METHODS:
if issubclass(exc_type, exc_handler['exc']):
if exc_handler['set_wrap']:
wrap = True
handler = exc_handler['handler']
ret = handler(request, user_message, redirect, ignore,
exc_handler.get('escalate', escalate),
handled, force_silence, force_log,
log_method, log_entry, log_level)
if ret:
return ret # return to normal code flow
# If we've gotten here, time to wrap and/or raise our exception.
if wrap:
raise HandledException([exc_type, exc_value, exc_traceback])
# assume exceptions handled in the code that pass in a message are already
# handled appropriately and treat as recoverable
if message:
ret = handle_recoverable(request, user_message, redirect, ignore,
escalate, handled, force_silence, force_log,
log_method, log_entry, log_level)
if ret:
return ret
six.reraise(exc_type, exc_value, exc_traceback)