# 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.conf import settings 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 horizon import exceptions from openstack_auth import utils as auth_utils from openstack_dashboard import policy import urllib from monitoring.alarms import tables as alarm_tables from monitoring import api 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']: dimension = metric['dimensions'][dim_name] dimensions.append(dimension) 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'] = str(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 '/' # encoding and decoding is required because of python3 compatibility # urlsafe_b64encode requires byte-type text name = 'b64:' + base64.urlsafe_b64encode(name.encode('utf-8')).decode('utf-8') 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'] = str(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_kibana'] = policy.check( ((getattr(settings, 'KIBANA_POLICY_SCOPE'), getattr(settings, 'KIBANA_POLICY_RULE')), ), self.request ) context['enable_log_management_button'] = settings.ENABLE_LOG_MANAGEMENT_BUTTON context['enable_event_management_button'] = settings.ENABLE_EVENT_MANAGEMENT_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] value = dimension_name_value[1] if isinstance(value, bytes): value = value.decode("utf-8") 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", ""), "Content-Type": "application/json", } 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 )