monasca-ui/monitoring/overview/views.py

436 lines
16 KiB
Python

# 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
)