# Copyright 2013 Hewlett-Packard Development Company, L.P. # # 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 base64 import copy import json import logging from django.contrib import messages from django import http from django.http import HttpResponse from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ # noqa from django.views.decorators.csrf import csrf_exempt from django.views import generic from django.views.generic import TemplateView from openstack_auth import utils as auth_utils from openstack_dashboard import policy import six from six.moves import urllib from horizon import exceptions from monitoring.alarms import tables as alarm_tables from monitoring import api from monitoring.config import local_settings as settings from monitoring.overview import constants LOG = logging.getLogger(__name__) STATUS_FA_ICON_MAP = {'btn-success': "fa-check", 'btn-danger': "fa-exclamation-triangle", 'btn-warning': "fa-exclamation", 'btn-default': "fa-question-circle"} def get_icon(status): return STATUS_FA_ICON_MAP.get(status, "fa-question-circle") priorities = [ {'status': 'btn-success', 'severity': 'OK'}, {'status': 'btn-default', 'severity': 'UNDETERMINED'}, {'status': 'btn-warning', 'severity': 'LOW'}, {'status': 'btn-warning', 'severity': 'MEDIUM'}, {'status': 'btn-warning', 'severity': 'HIGH'}, {'status': 'btn-danger', 'severity': 'CRITICAL'}, ] index_by_severity = {d['severity']: i for i, d in enumerate(priorities)} def get_dashboard_links(request): # # GRAFANA_LINKS is a list of dictionaries, but can either # be a nested list of dictionaries indexed by project name # (or '*'), or simply the list of links to display. This # code is a bit more complicated as a result but will allow # for backward compatibility and ensure existing installations # that don't take advantage of project specific dashboard # links are unaffected. The 'non_project_keys' are the # expected dictionary keys for the list of dashboard links, # so if we encounter one of those, we know we're supporting # legacy/non-project specific behavior. # # See examples of both in local_settings.py # non_project_keys = {'fileName', 'title'} try: for project_link in settings.DASHBOARDS: key = list(project_link)[0] value = list(project_link.values())[0] if key in non_project_keys: # # we're not indexed by project, just return # the whole list. # return settings.DASHBOARDS elif key == request.user.project_name: # # we match this project, return the project # specific links. # return value elif key == '*': # # this is a global setting, squirrel it away # in case we exhaust the list without a project # match # return value return settings.DEFAULT_LINKS except Exception: LOG.warning("Failed to parse dashboard links by project, returning defaults.") pass # # Extra safety here -- should have got a match somewhere above, # but fall back to defaults. # return settings.DASHBOARDS def get_monitoring_services(request): # # GRAFANA_LINKS is a list of dictionaries, but can either # be a nested list of dictionaries indexed by project name # (or '*'), or simply the list of links to display. This # code is a bit more complicated as a result but will allow # for backward compatibility and ensure existing installations # that don't take advantage of project specific dashboard # links are unaffected. The 'non_project_keys' are the # expected dictionary keys for the list of dashboard links, # so if we encounter one of those, we know we're supporting # legacy/non-project specific behavior. # # See examples of both in local_settings.py # non_project_keys = {'name', 'groupBy'} try: for group in settings.MONITORING_SERVICES: key = list(group.keys())[0] value = list(group.values())[0] if key in non_project_keys: # # we're not indexed by project, just return # the whole list. # return settings.MONITORING_SERVICES elif key == request.user.project_name: # # we match this project, return the project # specific links. # return value elif key == '*': # # this is a global setting, squirrel it away # in case we exhaust the list without a project # match # return value return settings.MONITORING_SERVICES except Exception: LOG.warning("Failed to parse monitoring services by project, returning defaults.") pass # # Extra safety here -- should have got a match somewhere above, # but fall back to defaults. # return settings.MONITORING_SERVICES def show_by_dimension(data, dim_name): if 'metrics' in data: dimensions = [] for metric in data['metrics']: if 'dimensions' in metric: if dim_name in metric['dimensions']: dimensions.append(str(metric['dimensions'][dim_name].encode('utf-8'))) return dimensions return [] def get_status(alarms): if not alarms: return 'chicklet-notfound' status_index = 0 for a in alarms: severity = alarm_tables.show_severity(a) severity_index = index_by_severity.get(severity, None) status_index = max(status_index, severity_index) return priorities[status_index]['status'] def generate_status(request): try: alarms = api.monitor.alarm_list(request) except Exception as e: messages.error(request, _('Unable to list alarms: %s') % str(e)) alarms = [] alarms_by_service = {} for a in alarms: service = alarm_tables.get_service(a) service_alarms = alarms_by_service.setdefault(service, []) service_alarms.append(a) monitoring_services = copy.deepcopy(get_monitoring_services(request)) for row in monitoring_services: row['name'] = six.text_type(row['name']) if 'groupBy' in row: alarms_by_group = {} for a in alarms: groups = show_by_dimension(a, row['groupBy']) if groups: for group in groups: group_alarms = alarms_by_group.setdefault(group, []) group_alarms.append(a) services = [] for group, group_alarms in alarms_by_group.items(): name = '%s=%s' % (row['groupBy'], group) # Encode as base64url to be able to include '/' name = 'b64:' + base64.urlsafe_b64encode(name) service = { 'display': group, 'name': name, 'class': get_status(group_alarms) } service['icon'] = get_icon(service['class']) services.append(service) row['services'] = services else: for service in row['services']: service_alarms = alarms_by_service.get(service['name'], []) service['class'] = get_status(service_alarms) service['icon'] = get_icon(service['class']) service['display'] = six.text_type(service['display']) return monitoring_services class IndexView(TemplateView): template_name = constants.TEMPLATE_PREFIX + 'index.html' def get_context_data(self, **kwargs): if not policy.check((('monitoring', 'monitoring:monitoring'), ), self.request): raise exceptions.NotAuthorized() context = super(IndexView, self).get_context_data(**kwargs) try: region = self.request.user.services_region context["grafana_url"] = getattr(settings, 'GRAFANA_URL').get(region, '') except AttributeError: # Catches case where Grafana 2 is not enabled. proxy_url_path = str(reverse_lazy(constants.URL_PREFIX + 'proxy')) api_root = self.request.build_absolute_uri(proxy_url_path) context["api"] = api_root context["dashboards"] = get_dashboard_links(self.request) # Ensure all links have a 'raw' attribute for link in context["dashboards"]: link['raw'] = link.get('raw', False) context['can_access_logs'] = policy.check( ((getattr(settings, 'KIBANA_POLICY_SCOPE'), getattr(settings, 'KIBANA_POLICY_RULE')), ), self.request ) context['enable_kibana_button'] = settings.ENABLE_KIBANA_BUTTON context['show_grafana_home'] = settings.SHOW_GRAFANA_HOME return context class MonascaProxyView(TemplateView): template_name = "" def _convert_dimensions(self, req_kwargs): """Converts the dimension string service:monitoring into a dict This method converts the dimension string service:monitoring (requested by a query string arg) into a python dict that looks like {"service": "monitoring"} (used by monasca api calls) """ dim_dict = {} if 'dimensions' in req_kwargs: dimensions_str = req_kwargs['dimensions'][0] dimensions_str_array = dimensions_str.split(',') for dimension in dimensions_str_array: # limit splitting since value may contain a ':' such as in # the `url` dimension of the service_status check. dimension_name_value = dimension.split(':', 1) if len(dimension_name_value) == 2: name = dimension_name_value[0].encode('utf8') value = dimension_name_value[1].encode('utf8') dim_dict[name] = urllib.parse.unquote(value) else: raise Exception('Dimensions are malformed') # # If the request specifies 'INJECT_REGION' as the region, we'll # replace with the horizon scoped region. We can't do this by # default, since some implementations don't publish region as a # dimension for all metrics (mini-mon for one). # if 'region' in dim_dict and dim_dict['region'] == 'INJECT_REGION': dim_dict['region'] = self.request.user.services_region req_kwargs['dimensions'] = dim_dict return req_kwargs def get(self, request, *args, **kwargs): # monasca_endpoint = api.monitor.monasca_endpoint(self.request) restpath = self.kwargs['restpath'] results = None parts = restpath.split('/') if "metrics" == parts[0]: req_kwargs = dict(self.request.GET) self._convert_dimensions(req_kwargs) if len(parts) == 1: results = {'elements': api.monitor. metrics_list(request, **req_kwargs)} elif "statistics" == parts[1]: results = {'elements': api.monitor. metrics_stat_list(request, **req_kwargs)} elif "measurements" == parts[1]: results = {'elements': api.monitor. metrics_measurement_list(request, **req_kwargs)} elif "dimensions" == parts[1]: results = {'elements': api.monitor. metrics_dimension_value_list(request, **req_kwargs)} if not results: LOG.warning("There was a request made for the path %s that" " is not supported." % restpath) results = {} return HttpResponse(json.dumps(results), content_type='application/json') class StatusView(TemplateView): template_name = "" def get(self, request, *args, **kwargs): ret = { 'series': generate_status(self.request), 'settings': {} } return HttpResponse(json.dumps(ret), content_type='application/json') class _HttpMethodRequest(urllib.request.Request): def __init__(self, method, url, **kwargs): urllib.request.Request.__init__(self, url, **kwargs) self.method = method def get_method(self): return self.method def proxy_stream_generator(response): while True: chunk = response.read(1000 * 1024) if not chunk: break yield chunk class KibanaProxyView(generic.View): base_url = None http_method_names = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD'] def read(self, method, url, data, headers): proxy_request_url = self.get_absolute_url(url) proxy_request = _HttpMethodRequest( method, proxy_request_url, data=data, headers=headers ) try: response = urllib.request.urlopen(proxy_request) except urllib.error.HTTPError as e: return http.HttpResponse( e.read(), status=e.code, content_type=e.hdrs['content-type'] ) except urllib.error.URLError as e: return http.HttpResponse(e.reason, 404) else: status = response.getcode() proxy_response = http.StreamingHttpResponse( proxy_stream_generator(response), status=status, content_type=response.headers['content-type'] ) if 'set-cookie' in response.headers: proxy_response['set-cookie'] = response.headers['set-cookie'] return proxy_response @csrf_exempt def dispatch(self, request, url): if not url: url = '/' if request.method not in self.http_method_names: return http.HttpResponseNotAllowed(request.method) if not self._can_access_kibana(): error_msg = (_('User %s does not have sufficient ' 'privileges to access Kibana') % auth_utils.get_user(request)) LOG.error(error_msg) return http.HttpResponseForbidden(content=error_msg) # passing kbn version explicitly for kibana >= 4.3.x headers = { 'X-Auth-Token': request.user.token.id, 'kbn-version': request.META.get('HTTP_KBN_VERSION', ''), 'Cookie': request.META.get('HTTP_COOKIE', '') } return self.read(request.method, url, request.body, headers) def get_relative_url(self, url): url = urllib.parse.quote(url.encode('utf-8')) params_str = self.request.GET.urlencode() if params_str: return '{0}?{1}'.format(url, params_str) return url def get_absolute_url(self, url): return self.base_url + self.get_relative_url(url).lstrip('/') def _can_access_kibana(self): return policy.check( ((getattr(settings, 'KIBANA_POLICY_SCOPE'), getattr(settings, 'KIBANA_POLICY_RULE')), ), self.request )