##// END OF EJS Templates
setup: change url to github
setup: change url to github

File last commit:

r153:32f4b641
r196:472d1df0 master
Show More
applications.py
846 lines | 27.6 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
# Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
#
# 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 copy
import json
import logging
from datetime import datetime, timedelta
import colander
from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
from pyramid.view import view_config
from webob.multidict import MultiDict
from zope.sqlalchemy import mark_changed
from ziggurat_foundations.permissions import ANY_PERMISSION
import appenlight.forms as forms
from appenlight.models import DBSession
from appenlight.models.resource import Resource
from appenlight.models.application import Application
from appenlight.models.application_postprocess_conf import ApplicationPostprocessConf
from ziggurat_foundations.models.services.user import UserService
from ziggurat_foundations.models.services.resource import ResourceService
from ziggurat_foundations.models.services.user_resource_permission import (
UserResourcePermissionService,
)
from appenlight.models.user_resource_permission import UserResourcePermission
from appenlight.models.group_resource_permission import GroupResourcePermission
from appenlight.models.services.application import ApplicationService
from appenlight.models.services.application_postprocess_conf import (
ApplicationPostprocessConfService,
)
from appenlight.models.services.group import GroupService
from appenlight.models.services.group_resource_permission import (
GroupResourcePermissionService,
)
from appenlight.models.services.request_metric import RequestMetricService
from appenlight.models.services.report_group import ReportGroupService
from appenlight.models.services.slow_call import SlowCallService
from appenlight.lib import helpers as h
from appenlight.lib.utils import build_filter_settings_from_query_dict
from appenlight.security import RootFactory
from appenlight.models.report import REPORT_TYPE_MATRIX
from appenlight.validators import build_rule_schema
_ = str
log = logging.getLogger(__name__)
def app_not_found(request, id):
"""
Redirects on non found and sets a flash message
"""
request.session.flash(_("Application not found"), "warning")
return HTTPFound(location=request.route_url("applications", action="index"))
@view_config(
route_name="applications_no_id",
renderer="json",
request_method="GET",
permission="authenticated",
)
def applications_list(request):
"""
Applications list
if query params contain ?type=foo, it will list applications
with one of those permissions for user,
otherwise only list of owned applications will
be returned
appending ?root_list while being administration will allow to list all
applications in the system
"""
is_root = request.has_permission("root_administration", RootFactory(request))
if is_root and request.GET.get("root_list"):
resources = Resource.all().order_by(Resource.resource_name)
resource_type = request.GET.get("resource_type", "application")
if resource_type:
resources = resources.filter(Resource.resource_type == resource_type)
else:
permissions = request.params.getall("permission")
if permissions:
resources = UserService.resources_with_perms(
request.user,
permissions,
resource_types=[request.GET.get("resource_type", "application")],
)
else:
resources = request.user.resources.filter(
Application.resource_type
== request.GET.get("resource_type", "application")
)
return [
r.get_dict(
include_keys=[
"resource_id",
"resource_name",
"domains",
"owner_user_name",
"owner_group_name",
]
)
for r in resources
]
@view_config(
route_name="applications", renderer="json", request_method="GET", permission="view"
)
def application_GET(request):
resource = request.context.resource
include_sensitive_info = False
if request.has_permission("edit"):
include_sensitive_info = True
resource_dict = resource.get_dict(
include_perms=include_sensitive_info,
include_processing_rules=include_sensitive_info,
)
return resource_dict
@view_config(
route_name="applications_no_id",
request_method="POST",
renderer="json",
permission="create_resources",
)
def application_create(request):
"""
Creates new application instances
"""
user = request.user
form = forms.ApplicationCreateForm(
MultiDict(request.unsafe_json_body), csrf_context=request
)
if form.validate():
session = DBSession()
resource = Application()
DBSession.add(resource)
form.populate_obj(resource)
resource.api_key = resource.generate_api_key()
user.resources.append(resource)
request.session.flash(_("Application created"))
DBSession.flush()
mark_changed(session)
else:
return HTTPUnprocessableEntity(body=form.errors_json)
return resource.get_dict()
@view_config(
route_name="applications",
request_method="PATCH",
renderer="json",
permission="edit",
)
def application_update(request):
"""
Updates main application configuration
"""
resource = request.context.resource
if not resource:
return app_not_found()
# disallow setting permanent storage by non-admins
# use default/non-resource based context for this check
req_dict = copy.copy(request.unsafe_json_body)
if not request.has_permission("root_administration", RootFactory(request)):
req_dict["allow_permanent_storage"] = ""
if not req_dict.get("uptime_url"):
# needed cause validator is still triggered by default
req_dict.pop("uptime_url", "")
application_form = forms.ApplicationUpdateForm(
MultiDict(req_dict), csrf_context=request
)
if application_form.validate():
application_form.populate_obj(resource)
request.session.flash(_("Application updated"))
else:
return HTTPUnprocessableEntity(body=application_form.errors_json)
include_sensitive_info = False
if request.has_permission("edit"):
include_sensitive_info = True
resource_dict = resource.get_dict(
include_perms=include_sensitive_info,
include_processing_rules=include_sensitive_info,
)
return resource_dict
@view_config(
route_name="applications_property",
match_param="key=api_key",
request_method="POST",
renderer="json",
permission="delete",
)
def application_regenerate_key(request):
"""
Regenerates API keys for application
"""
resource = request.context.resource
form = forms.CheckPasswordForm(
MultiDict(request.unsafe_json_body), csrf_context=request
)
form.password.user = request.user
if form.validate():
resource.api_key = resource.generate_api_key()
resource.public_key = resource.generate_api_key()
msg = "API keys regenerated - please update your application config."
request.session.flash(_(msg))
else:
return HTTPUnprocessableEntity(body=form.errors_json)
if request.has_permission("edit"):
include_sensitive_info = True
resource_dict = resource.get_dict(
include_perms=include_sensitive_info,
include_processing_rules=include_sensitive_info,
)
return resource_dict
@view_config(
route_name="applications_property",
match_param="key=delete_resource",
request_method="PATCH",
renderer="json",
permission="delete",
)
def application_remove(request):
"""
Removes application resources
"""
resource = request.context.resource
# we need polymorphic object here, to properly launch sqlalchemy events
resource = ApplicationService.by_id(resource.resource_id)
form = forms.CheckPasswordForm(
MultiDict(request.safe_json_body or {}), csrf_context=request
)
form.password.user = request.user
if form.validate():
DBSession.delete(resource)
request.session.flash(_("Application removed"))
else:
return HTTPUnprocessableEntity(body=form.errors_json)
return True
@view_config(
route_name="applications_property",
match_param="key=owner",
request_method="PATCH",
renderer="json",
permission="delete",
)
def application_ownership_transfer(request):
"""
Allows application owner to transfer application ownership to other user
"""
resource = request.context.resource
form = forms.ChangeApplicationOwnerForm(
MultiDict(request.safe_json_body or {}), csrf_context=request
)
form.password.user = request.user
if form.validate():
user = UserService.by_user_name(form.user_name.data)
user.resources.append(resource)
# remove integrations to not leak security data of external applications
for integration in resource.integrations[:]:
resource.integrations.remove(integration)
request.session.flash(_("Application transfered"))
else:
return HTTPUnprocessableEntity(body=form.errors_json)
return True
@view_config(
route_name="applications_property",
match_param="key=postprocessing_rules",
renderer="json",
request_method="POST",
permission="edit",
)
def applications_postprocess_POST(request):
"""
Creates new postprocessing rules for applications
"""
resource = request.context.resource
conf = ApplicationPostprocessConf()
conf.do = "postprocess"
conf.new_value = "1"
resource.postprocess_conf.append(conf)
DBSession.flush()
return conf.get_dict()
@view_config(
route_name="applications_property",
match_param="key=postprocessing_rules",
renderer="json",
request_method="PATCH",
permission="edit",
)
def applications_postprocess_PATCH(request):
"""
Creates new postprocessing rules for applications
"""
json_body = request.unsafe_json_body
schema = build_rule_schema(json_body["rule"], REPORT_TYPE_MATRIX)
try:
schema.deserialize(json_body["rule"])
except colander.Invalid as exc:
return HTTPUnprocessableEntity(body=json.dumps(exc.asdict()))
resource = request.context.resource
conf = ApplicationPostprocessConfService.by_pkey_and_resource_id(
json_body["pkey"], resource.resource_id
)
conf.rule = request.unsafe_json_body["rule"]
# for now hardcode int since we dont support anything else so far
conf.new_value = int(request.unsafe_json_body["new_value"])
return conf.get_dict()
@view_config(
route_name="applications_property",
match_param="key=postprocessing_rules",
renderer="json",
request_method="DELETE",
permission="edit",
)
def applications_postprocess_DELETE(request):
"""
Removes application postprocessing rules
"""
form = forms.ReactorForm(request.POST, csrf_context=request)
resource = request.context.resource
if form.validate():
for postprocess_conf in resource.postprocess_conf:
if postprocess_conf.pkey == int(request.GET["pkey"]):
# remove rule
DBSession.delete(postprocess_conf)
return True
@view_config(
route_name="applications_property",
match_param="key=report_graphs",
renderer="json",
permission="view",
)
@view_config(
route_name="applications_property",
match_param="key=slow_report_graphs",
renderer="json",
permission="view",
)
def get_application_report_stats(request):
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
if not filter_settings.get("start_date"):
delta = timedelta(hours=1)
filter_settings["start_date"] = filter_settings["end_date"] - delta
result = ReportGroupService.get_report_stats(request, filter_settings)
return result
@view_config(
route_name="applications_property",
match_param="key=metrics_graphs",
renderer="json",
permission="view",
)
def metrics_graphs(request):
"""
Handles metric dashboard graphs
Returns information for time/tier breakdown
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
delta = timedelta(hours=1)
if not filter_settings.get("start_date"):
filter_settings["start_date"] = filter_settings["end_date"] - delta
if filter_settings["end_date"] <= filter_settings["start_date"]:
filter_settings["end_date"] = filter_settings["start_date"]
delta = filter_settings["end_date"] - filter_settings["start_date"]
if delta < h.time_deltas.get("12h")["delta"]:
divide_by_min = 1
elif delta <= h.time_deltas.get("3d")["delta"]:
divide_by_min = 5.0
elif delta >= h.time_deltas.get("2w")["delta"]:
divide_by_min = 60.0 * 24
else:
divide_by_min = 60.0
results = RequestMetricService.get_metrics_stats(request, filter_settings)
# because requests are PER SECOND / we divide 1 min stats by 60
# requests are normalized to 1 min average
# results are average seconds time spent per request in specific area
for point in results:
if point["requests"]:
point["main"] = (
point["main"]
- point["sql"]
- point["nosql"]
- point["remote"]
- point["tmpl"]
- point["custom"]
) / point["requests"]
point["sql"] = point["sql"] / point["requests"]
point["nosql"] = point["nosql"] / point["requests"]
point["remote"] = point["remote"] / point["requests"]
point["tmpl"] = point["tmpl"] / point["requests"]
point["custom"] = point["custom"] / point["requests"]
point["requests_2"] = point["requests"] / 60.0 / divide_by_min
selected_types = ["main", "sql", "nosql", "remote", "tmpl", "custom"]
for point in results:
for stat_type in selected_types:
point[stat_type] = round(point.get(stat_type, 0), 3)
return results
@view_config(
route_name="applications_property",
match_param="key=response_graphs",
renderer="json",
permission="view",
)
def response_graphs(request):
"""
Handles dashboard infomation for avg. response time split by today,
2 days ago and week ago
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
delta = timedelta(hours=1)
if not filter_settings.get("start_date"):
filter_settings["start_date"] = filter_settings["end_date"] - delta
result_now = RequestMetricService.get_metrics_stats(request, filter_settings)
filter_settings_2d = filter_settings.copy()
filter_settings_2d["start_date"] = filter_settings["start_date"] - timedelta(days=2)
filter_settings_2d["end_date"] = filter_settings["end_date"] - timedelta(days=2)
result_2d = RequestMetricService.get_metrics_stats(request, filter_settings_2d)
filter_settings_7d = filter_settings.copy()
filter_settings_7d["start_date"] = filter_settings["start_date"] - timedelta(days=7)
filter_settings_7d["end_date"] = filter_settings["end_date"] - timedelta(days=7)
result_7d = RequestMetricService.get_metrics_stats(request, filter_settings_7d)
plot_data = []
for item in result_now:
point = {"x": item["x"], "today": 0, "days_ago_2": 0, "days_ago_7": 0}
if item["requests"]:
point["today"] = round(item["main"] / item["requests"], 3)
plot_data.append(point)
for i, item in enumerate(result_2d[: len(plot_data)]):
plot_data[i]["days_ago_2"] = 0
point = result_2d[i]
if point["requests"]:
plot_data[i]["days_ago_2"] = round(point["main"] / point["requests"], 3)
for i, item in enumerate(result_7d[: len(plot_data)]):
plot_data[i]["days_ago_7"] = 0
point = result_7d[i]
if point["requests"]:
plot_data[i]["days_ago_7"] = round(point["main"] / point["requests"], 3)
return plot_data
@view_config(
route_name="applications_property",
match_param="key=requests_graphs",
renderer="json",
permission="view",
)
def requests_graphs(request):
"""
Handles dashboard infomation for avg. response time split by today,
2 days ago and week ago
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
delta = timedelta(hours=1)
if not filter_settings.get("start_date"):
filter_settings["start_date"] = filter_settings["end_date"] - delta
result_now = RequestMetricService.get_metrics_stats(request, filter_settings)
delta = filter_settings["end_date"] - filter_settings["start_date"]
if delta < h.time_deltas.get("12h")["delta"]:
seconds = h.time_deltas["1m"]["minutes"] * 60.0
elif delta <= h.time_deltas.get("3d")["delta"]:
seconds = h.time_deltas["5m"]["minutes"] * 60.0
elif delta >= h.time_deltas.get("2w")["delta"]:
seconds = h.time_deltas["24h"]["minutes"] * 60.0
else:
seconds = h.time_deltas["1h"]["minutes"] * 60.0
for item in result_now:
if item["requests"]:
item["requests"] = round(item["requests"] / seconds, 3)
return result_now
@view_config(
route_name="applications_property",
match_param="key=apdex_stats",
renderer="json",
permission="view",
)
def get_apdex_stats(request):
"""
Returns information and calculates APDEX score per server for dashboard
server information (upper right stats boxes)
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
# make sure we have only one resource here to don't produce
# weird results when we have wrong app in app selector
filter_settings["resource"] = [filter_settings["resource"][0]]
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
delta = timedelta(hours=1)
if not filter_settings.get("start_date"):
filter_settings["start_date"] = filter_settings["end_date"] - delta
return RequestMetricService.get_apdex_stats(request, filter_settings)
@view_config(
route_name="applications_property",
match_param="key=slow_calls",
renderer="json",
permission="view",
)
def get_slow_calls(request):
"""
Returns information for time consuming calls in specific time interval
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
delta = timedelta(hours=1)
if not filter_settings.get("start_date"):
filter_settings["start_date"] = filter_settings["end_date"] - delta
return SlowCallService.get_time_consuming_calls(request, filter_settings)
@view_config(
route_name="applications_property",
match_param="key=requests_breakdown",
renderer="json",
permission="view",
)
def get_requests_breakdown(request):
"""
Used on dashboard to get information which views are most used in
a time interval
"""
query_params = request.GET.mixed()
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
if not filter_settings.get("start_date"):
delta = timedelta(hours=1)
filter_settings["start_date"] = filter_settings["end_date"] - delta
series = RequestMetricService.get_requests_breakdown(request, filter_settings)
results = []
for row in series:
d_row = {
"avg_response": round(row["main"] / row["requests"], 3),
"requests": row["requests"],
"main": row["main"],
"view_name": row["key"],
"latest_details": row["latest_details"],
"percentage": round(row["percentage"] * 100, 1),
}
results.append(d_row)
return results
@view_config(
route_name="applications_property",
match_param="key=trending_reports",
renderer="json",
permission="view",
)
def trending_reports(request):
"""
Returns exception/slow reports trending for specific time interval
"""
query_params = request.GET.mixed().copy()
# pop report type to rewrite it to tag later
report_type = query_params.pop("report_type", None)
if report_type:
query_params["type"] = report_type
query_params["resource"] = (request.context.resource.resource_id,)
filter_settings = build_filter_settings_from_query_dict(request, query_params)
if not filter_settings.get("end_date"):
end_date = datetime.utcnow().replace(microsecond=0, second=0)
filter_settings["end_date"] = end_date
if not filter_settings.get("start_date"):
delta = timedelta(hours=1)
filter_settings["start_date"] = filter_settings["end_date"] - delta
results = ReportGroupService.get_trending(request, filter_settings)
trending = []
for occurences, group in results:
report_group = group.get_dict(request)
# show the occurences in time range instead of global ones
report_group["occurences"] = occurences
trending.append(report_group)
return trending
@view_config(
route_name="applications_property",
match_param="key=integrations",
renderer="json",
permission="view",
)
def integrations(request):
"""
Integration list for given application
"""
application = request.context.resource
return {"resource": application}
@view_config(
route_name="applications_property",
match_param="key=user_permissions",
renderer="json",
permission="owner",
request_method="POST",
)
def user_resource_permission_create(request):
"""
Set new permissions for user for a resource
"""
resource = request.context.resource
user_name = request.unsafe_json_body.get("user_name")
user = UserService.by_user_name(user_name)
if not user:
user = UserService.by_email(user_name)
if not user:
return False
for perm_name in request.unsafe_json_body.get("permissions", []):
permission = UserResourcePermissionService.by_resource_user_and_perm(
user.id, perm_name, resource.resource_id
)
if not permission:
permission = UserResourcePermission(perm_name=perm_name, user_id=user.id)
resource.user_permissions.append(permission)
DBSession.flush()
perms = [
p.perm_name
for p in ResourceService.perms_for_user(resource, user)
if p.type == "user"
]
result = {"user_name": user.user_name, "permissions": list(set(perms))}
return result
@view_config(
route_name="applications_property",
match_param="key=user_permissions",
renderer="json",
permission="owner",
request_method="DELETE",
)
def user_resource_permission_delete(request):
"""
Removes user permission from specific resource
"""
resource = request.context.resource
user = UserService.by_user_name(request.GET.get("user_name"))
if not user:
return False
for perm_name in request.GET.getall("permissions"):
permission = UserResourcePermissionService.by_resource_user_and_perm(
user.id, perm_name, resource.resource_id
)
resource.user_permissions.remove(permission)
DBSession.flush()
perms = [
p.perm_name
for p in ResourceService.perms_for_user(resource, user)
if p.type == "user"
]
result = {"user_name": user.user_name, "permissions": list(set(perms))}
return result
@view_config(
route_name="applications_property",
match_param="key=group_permissions",
renderer="json",
permission="owner",
request_method="POST",
)
def group_resource_permission_create(request):
"""
Set new permissions for group for a resource
"""
resource = request.context.resource
group = GroupService.by_id(request.unsafe_json_body.get("group_id"))
if not group:
return False
for perm_name in request.unsafe_json_body.get("permissions", []):
permission = GroupResourcePermissionService.by_resource_group_and_perm(
group.id, perm_name, resource.resource_id
)
if not permission:
permission = GroupResourcePermission(perm_name=perm_name, group_id=group.id)
resource.group_permissions.append(permission)
DBSession.flush()
perm_tuples = ResourceService.groups_for_perm(
resource, ANY_PERMISSION, limit_group_permissions=True, group_ids=[group.id]
)
perms = [p.perm_name for p in perm_tuples if p.type == "group"]
result = {"group": group.get_dict(), "permissions": list(set(perms))}
return result
@view_config(
route_name="applications_property",
match_param="key=group_permissions",
renderer="json",
permission="owner",
request_method="DELETE",
)
def group_resource_permission_delete(request):
"""
Removes group permission from specific resource
"""
form = forms.ReactorForm(request.POST, csrf_context=request)
form.validate()
resource = request.context.resource
group = GroupService.by_id(request.GET.get("group_id"))
if not group:
return False
for perm_name in request.GET.getall("permissions"):
permission = GroupResourcePermissionService.by_resource_group_and_perm(
group.id, perm_name, resource.resource_id
)
resource.group_permissions.remove(permission)
DBSession.flush()
perm_tuples = ResourceService.groups_for_perm(
resource, ANY_PERMISSION, limit_group_permissions=True, group_ids=[group.id]
)
perms = [p.perm_name for p in perm_tuples if p.type == "group"]
result = {"group": group.get_dict(), "permissions": list(set(perms))}
return result