Co-authored-by: Eve832 <eve.meelan@grafana.com>
Co-authored-by: Francisco Montes de Oca <nevermind89x@gmail.com>
Co-authored-by: Ildar Iskhakov <ildar.iskhakov@grafana.com>
Co-authored-by: Innokentii Konstantinov <innokenty.konstantinov@grafana.com>
Co-authored-by: Julia <ferril.darkdiver@gmail.com>
Co-authored-by: maskin25 <kengurek@gmail.com>
Co-authored-by: Matias Bordese <mbordese@gmail.com>
Co-authored-by: Matvey Kukuy <motakuk@gmail.com>
Co-authored-by: Michael Derynck <michael.derynck@grafana.com>
Co-authored-by: Richard Hartmann <richih@richih.org>
Co-authored-by: Robby Milo <robbymilo@fastmail.com>
Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>
Co-authored-by: Vadim Stepanov <vadimkerr@gmail.com>
Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
571 lines
24 KiB
Python
571 lines
24 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
from django import forms
|
|
from django.db import models
|
|
from django.db.models import CharField, Q
|
|
from django.db.models.constants import LOOKUP_SEP
|
|
from django.db.models.functions import Cast
|
|
from django.utils import timezone
|
|
from django_filters import rest_framework as filters
|
|
from django_filters.widgets import RangeWidget
|
|
from rest_framework import mixins, status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.filters import SearchFilter
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from apps.alerts.constants import ActionSource
|
|
from apps.alerts.models import AlertGroup, AlertReceiveChannel
|
|
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
|
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
|
|
from apps.api.serializers.alert_group import AlertGroupSerializer
|
|
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
|
from apps.user_management.models import User
|
|
from common.api_helpers.exceptions import BadRequest
|
|
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
|
|
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin
|
|
from common.api_helpers.paginators import FiftyPageSizePaginator
|
|
|
|
|
|
def get_integration_queryset(request):
|
|
if request is None:
|
|
return AlertReceiveChannel.objects.none()
|
|
|
|
return AlertReceiveChannel.objects_with_maintenance.filter(organization=request.user.organization)
|
|
|
|
|
|
def get_user_queryset(request):
|
|
if request is None:
|
|
return User.objects.none()
|
|
|
|
return User.objects.filter(organization=request.user.organization).distinct()
|
|
|
|
|
|
class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
|
|
"""
|
|
Examples of possible date formats here https://docs.djangoproject.com/en/1.9/ref/settings/#datetime-input-formats
|
|
"""
|
|
|
|
started_at_gte = filters.DateTimeFilter(field_name="started_at", lookup_expr="gte")
|
|
started_at_lte = filters.DateTimeFilter(field_name="started_at", lookup_expr="lte")
|
|
resolved_at_lte = filters.DateTimeFilter(field_name="resolved_at", lookup_expr="lte")
|
|
is_root = filters.BooleanFilter(field_name="root_alert_group", lookup_expr="isnull")
|
|
id__in = filters.BaseInFilter(field_name="public_primary_key", lookup_expr="in")
|
|
status = filters.MultipleChoiceFilter(choices=AlertGroup.STATUS_CHOICES, method="filter_status")
|
|
started_at = filters.CharFilter(field_name="started_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
|
resolved_at = filters.CharFilter(field_name="resolved_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
|
silenced_at = filters.CharFilter(field_name="silenced_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
|
silenced_by = filters.ModelMultipleChoiceFilter(
|
|
field_name="silenced_by_user",
|
|
queryset=get_user_queryset,
|
|
to_field_name="public_primary_key",
|
|
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
|
)
|
|
integration = filters.ModelMultipleChoiceFilter(
|
|
field_name="channel_filter__alert_receive_channel",
|
|
queryset=get_integration_queryset,
|
|
to_field_name="public_primary_key",
|
|
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
|
)
|
|
started_at_range = filters.DateFromToRangeFilter(
|
|
field_name="started_at", widget=RangeWidget(attrs={"type": "date"})
|
|
)
|
|
resolved_by = filters.ModelMultipleChoiceFilter(
|
|
field_name="resolved_by_user",
|
|
queryset=get_user_queryset,
|
|
to_field_name="public_primary_key",
|
|
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
|
)
|
|
acknowledged_by = filters.ModelMultipleChoiceFilter(
|
|
field_name="acknowledged_by_user",
|
|
queryset=get_user_queryset,
|
|
to_field_name="public_primary_key",
|
|
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
|
)
|
|
invitees_are = filters.ModelMultipleChoiceFilter(
|
|
queryset=get_user_queryset, to_field_name="public_primary_key", method="filter_invitees_are"
|
|
)
|
|
with_resolution_note = filters.BooleanFilter(method="filter_with_resolution_note")
|
|
|
|
class Meta:
|
|
model = AlertGroup
|
|
fields = [
|
|
"id__in",
|
|
"resolved",
|
|
"acknowledged",
|
|
"started_at_gte",
|
|
"started_at_lte",
|
|
"resolved_at_lte",
|
|
"is_root",
|
|
"resolved_by",
|
|
"acknowledged_by",
|
|
]
|
|
|
|
def filter_status(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
try:
|
|
statuses = list(map(int, value))
|
|
except ValueError:
|
|
raise BadRequest(detail="Invalid status value")
|
|
|
|
filters = {}
|
|
q_objects = Q()
|
|
|
|
if AlertGroup.NEW in statuses:
|
|
filters["new"] = Q(silenced=False) & Q(acknowledged=False) & Q(resolved=False)
|
|
if AlertGroup.SILENCED in statuses:
|
|
filters["silenced"] = Q(silenced=True) & Q(acknowledged=False) & Q(resolved=False)
|
|
if AlertGroup.ACKNOWLEDGED in statuses:
|
|
filters["acknowledged"] = Q(acknowledged=True) & Q(resolved=False)
|
|
if AlertGroup.RESOLVED in statuses:
|
|
filters["resolved"] = Q(resolved=True)
|
|
|
|
for item in filters:
|
|
q_objects |= filters[item]
|
|
|
|
queryset = queryset.filter(q_objects)
|
|
|
|
return queryset
|
|
|
|
def filter_invitees_are(self, queryset, name, value):
|
|
users = value
|
|
|
|
if not users:
|
|
return queryset
|
|
|
|
queryset = queryset.filter(acknowledged=False, resolved=False, log_records__author__in=users).distinct()
|
|
|
|
return queryset
|
|
|
|
def filter_with_resolution_note(self, queryset, name, value):
|
|
if value is True:
|
|
queryset = queryset.filter(Q(resolution_notes__isnull=False, resolution_notes__deleted_at=None)).distinct()
|
|
elif value is False:
|
|
queryset = queryset.filter(
|
|
Q(resolution_notes__isnull=True) | ~Q(resolution_notes__deleted_at=None)
|
|
).distinct()
|
|
return queryset
|
|
|
|
|
|
class CustomSearchFilter(SearchFilter):
|
|
def must_call_distinct(self, queryset, search_fields):
|
|
"""
|
|
Return True if 'distinct()' should be used to query the given lookups.
|
|
"""
|
|
for search_field in search_fields:
|
|
opts = queryset.model._meta
|
|
if search_field[0] in self.lookup_prefixes:
|
|
search_field = search_field[1:]
|
|
|
|
# From https://github.com/encode/django-rest-framework/pull/6240/files#diff-01f357e474dd8fd702e4951b9227bffcR88
|
|
# Annotated fields do not need to be distinct
|
|
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
|
|
continue
|
|
|
|
parts = search_field.split(LOOKUP_SEP)
|
|
for part in parts:
|
|
field = opts.get_field(part)
|
|
if hasattr(field, "get_path_info"):
|
|
# This field is a relation, update opts to follow the relation
|
|
path_info = field.get_path_info()
|
|
opts = path_info[-1].to_opts
|
|
if any(path.m2m for path in path_info):
|
|
# This field is a m2m relation so we know we need to call distinct
|
|
return True
|
|
return False
|
|
|
|
|
|
class AlertGroupView(
|
|
PreviewTemplateMixin,
|
|
PublicPrimaryKeyMixin,
|
|
mixins.RetrieveModelMixin,
|
|
mixins.ListModelMixin,
|
|
viewsets.GenericViewSet,
|
|
):
|
|
authentication_classes = (
|
|
MobileAppAuthTokenAuthentication,
|
|
PluginAuthentication,
|
|
)
|
|
permission_classes = (IsAuthenticated, ActionPermission)
|
|
|
|
action_permissions = {
|
|
IsAdminOrEditor: (
|
|
*MODIFY_ACTIONS,
|
|
"acknowledge",
|
|
"unacknowledge",
|
|
"resolve",
|
|
"unresolve",
|
|
"attach",
|
|
"unattach",
|
|
"silence",
|
|
"unsilence",
|
|
"bulk_action",
|
|
"preview_template",
|
|
),
|
|
AnyRole: (
|
|
*READ_ACTIONS,
|
|
"stats",
|
|
"filters",
|
|
"silence_options",
|
|
"bulk_action_options",
|
|
),
|
|
}
|
|
|
|
http_method_names = ["get", "post"]
|
|
|
|
serializer_class = AlertGroupSerializer
|
|
|
|
pagination_class = FiftyPageSizePaginator
|
|
|
|
filter_backends = [CustomSearchFilter, filters.DjangoFilterBackend]
|
|
search_fields = ["cached_render_for_web_str"]
|
|
|
|
filterset_class = AlertGroupFilter
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
"""
|
|
It's compute-heavy so we rely on cache here.
|
|
Attention: Make sure to invalidate cache if you update the format!
|
|
"""
|
|
queryset = self.filter_queryset(self.get_queryset(eager=False, readonly=True))
|
|
|
|
page = self.paginate_queryset(queryset)
|
|
skip_slow_rendering = request.query_params.get("skip_slow_rendering") == "true"
|
|
data = []
|
|
|
|
for alert_group in page:
|
|
if alert_group.cached_render_for_web == {}:
|
|
# We cannot give empty data to web. So caching synchronously here.
|
|
if skip_slow_rendering:
|
|
# We just return dummy data.
|
|
# Cache is not launched because after skip_slow_rendering request should come usual one
|
|
# which will start caching
|
|
data.append({"pk": alert_group.pk, "short": True})
|
|
else:
|
|
# Synchronously cache and return. It could be slow.
|
|
alert_group.cache_for_web(alert_group.channel.organization)
|
|
data.append(alert_group.cached_render_for_web)
|
|
else:
|
|
data.append(alert_group.cached_render_for_web)
|
|
if not skip_slow_rendering:
|
|
# Cache is not launched because after skip_slow_rendering request should come usual one
|
|
# which will start caching
|
|
alert_group.schedule_cache_for_web()
|
|
|
|
return self.get_paginated_response(data)
|
|
|
|
def get_queryset(self, eager=True, readonly=False, order=True):
|
|
if readonly:
|
|
queryset = AlertGroup.unarchived_objects.using_readonly_db
|
|
else:
|
|
queryset = AlertGroup.unarchived_objects
|
|
|
|
queryset = queryset.filter(
|
|
channel__organization=self.request.auth.organization,
|
|
channel__team=self.request.user.current_team,
|
|
)
|
|
|
|
if order:
|
|
queryset = queryset.order_by("-started_at")
|
|
|
|
queryset = queryset.annotate(cached_render_for_web_str=Cast("cached_render_for_web", output_field=CharField()))
|
|
|
|
if eager:
|
|
queryset = self.serializer_class.setup_eager_loading(queryset)
|
|
return queryset
|
|
|
|
def get_alert_groups_and_days_for_previous_same_period(self):
|
|
prev_alert_groups = AlertGroup.unarchived_objects.none()
|
|
delta_days = None
|
|
|
|
started_at = self.request.query_params.get("started_at", None)
|
|
if started_at is not None:
|
|
started_at_gte, started_at_lte = AlertGroupFilter.parse_custom_datetime_range(started_at)
|
|
delta_days = None
|
|
if started_at_lte is not None:
|
|
started_at_lte = forms.DateTimeField().to_python(started_at_lte)
|
|
else:
|
|
started_at_lte = datetime.now()
|
|
|
|
if started_at_gte is not None:
|
|
started_at_gte = forms.DateTimeField().to_python(value=started_at_gte)
|
|
delta = started_at_lte.replace(tzinfo=None) - started_at_gte.replace(tzinfo=None)
|
|
prev_alert_groups = self.get_queryset().filter(
|
|
started_at__range=[started_at_gte - delta, started_at_gte]
|
|
)
|
|
delta_days = delta.days
|
|
return prev_alert_groups, delta_days
|
|
|
|
@action(detail=False)
|
|
def stats(self, *args, **kwargs):
|
|
alert_groups = self.filter_queryset(self.get_queryset(eager=False))
|
|
# Only count field is used, other fields left just in case for the backward compatibility
|
|
return Response(
|
|
{
|
|
"count": alert_groups.filter().count(),
|
|
"count_previous_same_period": 0,
|
|
"alert_group_rate_to_previous_same_period": 1,
|
|
"count_escalations": 0,
|
|
"count_escalations_previous_same_period": 0,
|
|
"escalation_rate_to_previous_same_period": 1,
|
|
"average_response_time": None,
|
|
"average_response_time_to_previous_same_period": None,
|
|
"average_response_time_rate_to_previous_same_period": 0,
|
|
"prev_period_in_days": 1,
|
|
}
|
|
)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def acknowledge(self, request, pk):
|
|
alert_group = self.get_object()
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't acknowledge maintenance alert group")
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
|
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unacknowledge(self, request, pk):
|
|
alert_group = self.get_object()
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't unacknowledge maintenance alert group")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't unacknowledge an attached alert group")
|
|
|
|
if not alert_group.acknowledged:
|
|
raise BadRequest(detail="The alert group is not acknowledged")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
|
|
|
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def resolve(self, request, pk):
|
|
alert_group = self.get_object()
|
|
organization = self.request.user.organization
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't resolve an attached alert group")
|
|
|
|
if alert_group.is_maintenance_incident:
|
|
alert_group.stop_maintenance(self.request.user)
|
|
else:
|
|
if organization.is_resolution_note_required and not alert_group.has_resolution_notes:
|
|
return Response(
|
|
data="Alert group without resolution note cannot be resolved due to organization settings.",
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unresolve(self, request, pk):
|
|
alert_group = self.get_object()
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't unresolve maintenance alert group")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't unresolve an attached alert group")
|
|
|
|
if not alert_group.resolved:
|
|
raise BadRequest(detail="The alert group is not resolved")
|
|
|
|
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def attach(self, request, pk=None):
|
|
alert_group = self.get_object()
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't attach maintenance alert group")
|
|
if alert_group.dependent_alert_groups.count() > 0:
|
|
raise BadRequest(detail="Can't attach an alert group because it has another alert groups attached to it")
|
|
if not alert_group.is_root_alert_group:
|
|
raise BadRequest(detail="Can't attach an alert group because it has already been attached")
|
|
|
|
try:
|
|
root_alert_group = self.get_queryset().get(public_primary_key=request.data["root_alert_group_pk"])
|
|
except AlertGroup.DoesNotExist:
|
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
if root_alert_group.resolved or root_alert_group.root_alert_group is not None:
|
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
if root_alert_group == alert_group:
|
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
alert_group.attach_by_user(self.request.user, root_alert_group, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group.pk)
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unattach(self, request, pk=None):
|
|
alert_group = self.get_object()
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't unattach maintenance alert group")
|
|
if alert_group.is_root_alert_group:
|
|
raise BadRequest(detail="Can't unattach an alert group because it is not attached")
|
|
root_alert_group_pk = alert_group.root_alert_group_id
|
|
alert_group.un_attach_by_user(self.request.user, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group_pk)
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def silence(self, request, pk=None):
|
|
alert_group = self.get_object()
|
|
|
|
delay = request.data.get("delay")
|
|
if delay is None:
|
|
raise BadRequest(detail="Please specify a delay for silence")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't silence an attached alert group")
|
|
|
|
alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB)
|
|
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
|
|
|
@action(methods=["get"], detail=False)
|
|
def silence_options(self, request):
|
|
data = [
|
|
{"value": value, "display_name": display_name} for value, display_name in AlertGroup.SILENCE_DELAY_OPTIONS
|
|
]
|
|
return Response(data)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unsilence(self, request, pk=None):
|
|
alert_group = self.get_object()
|
|
|
|
if not alert_group.silenced:
|
|
raise BadRequest(detail="The alert group is not silenced")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't unsilence a resolved alert group")
|
|
|
|
if alert_group.acknowledged:
|
|
raise BadRequest(detail="Can't unsilence an acknowledged alert group")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't unsilence an attached alert group")
|
|
|
|
alert_group.un_silence_by_user(request.user, action_source=ActionSource.WEB)
|
|
|
|
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
|
|
|
@action(methods=["get"], detail=False)
|
|
def filters(self, request):
|
|
filter_name = request.query_params.get("search", None)
|
|
api_root = "/api/internal/v1/"
|
|
|
|
now = timezone.now()
|
|
week_ago = now - timedelta(days=7)
|
|
|
|
default_datetime_range = "{}/{}".format(
|
|
week_ago.strftime(DateRangeFilterMixin.DATE_FORMAT),
|
|
now.strftime(DateRangeFilterMixin.DATE_FORMAT),
|
|
)
|
|
|
|
filter_options = [
|
|
{"name": "search", "type": "search"},
|
|
{"name": "integration", "type": "options", "href": api_root + "alert_receive_channels/?filters=true"},
|
|
{
|
|
"name": "acknowledged_by",
|
|
"type": "options",
|
|
"href": api_root + "users/?filters=true&roles=0&roles=1&roles=2",
|
|
"default": {"display_name": self.request.user.username, "value": self.request.user.public_primary_key},
|
|
},
|
|
{
|
|
"name": "resolved_by",
|
|
"type": "options",
|
|
"href": api_root + "users/?filters=true&roles=0&roles=1&roles=2",
|
|
},
|
|
{
|
|
"name": "silenced_by",
|
|
"type": "options",
|
|
"href": api_root + "users/?filters=true&roles=0&roles=1&roles=2",
|
|
},
|
|
{
|
|
"name": "invitees_are",
|
|
"type": "options",
|
|
"href": api_root + "users/?filters=true&roles=0&roles=1&roles=2",
|
|
},
|
|
{
|
|
"name": "status",
|
|
"type": "options",
|
|
"options": [
|
|
{"display_name": "new", "value": AlertGroup.NEW},
|
|
{"display_name": "acknowledged", "value": AlertGroup.ACKNOWLEDGED},
|
|
{"display_name": "resolved", "value": AlertGroup.RESOLVED},
|
|
{"display_name": "silenced", "value": AlertGroup.SILENCED},
|
|
],
|
|
},
|
|
# {'name': 'is_root', 'type': 'boolean', 'default': True},
|
|
{
|
|
"name": "started_at",
|
|
"type": "daterange",
|
|
"default": default_datetime_range,
|
|
},
|
|
{
|
|
"name": "resolved_at",
|
|
"type": "daterange",
|
|
"default": default_datetime_range,
|
|
},
|
|
{
|
|
"name": "with_resolution_note",
|
|
"type": "boolean",
|
|
"default": "true",
|
|
},
|
|
]
|
|
|
|
if filter_name is not None:
|
|
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
|
|
|
return Response(filter_options)
|
|
|
|
@action(methods=["post"], detail=False)
|
|
def bulk_action(self, request):
|
|
alert_group_public_pks = self.request.data.get("alert_group_pks", [])
|
|
action_with_incidents = self.request.data.get("action", None)
|
|
delay = self.request.data.get("delay")
|
|
kwargs = {}
|
|
|
|
if action_with_incidents not in AlertGroup.BULK_ACTIONS:
|
|
return Response("Unknown action", status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if action_with_incidents == AlertGroup.SILENCE:
|
|
if delay is None:
|
|
raise BadRequest(detail="Please specify a delay for silence")
|
|
kwargs["silence_delay"] = delay
|
|
|
|
alert_groups = self.get_queryset(eager=False).filter(public_primary_key__in=alert_group_public_pks)
|
|
alert_group_pks = list(alert_groups.values_list("id", flat=True))
|
|
invalidate_web_cache_for_alert_group(alert_group_pks=alert_group_pks)
|
|
|
|
kwargs["user"] = self.request.user
|
|
kwargs["alert_groups"] = alert_groups
|
|
|
|
method = getattr(AlertGroup, f"bulk_{action_with_incidents}")
|
|
method(**kwargs)
|
|
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["get"], detail=False)
|
|
def bulk_action_options(self, request):
|
|
return Response(
|
|
[{"value": action_name, "display_name": action_name} for action_name in AlertGroup.BULK_ACTIONS]
|
|
)
|
|
|
|
# This method is required for PreviewTemplateMixin
|
|
def get_alert_to_template(self):
|
|
return self.get_object().alerts.first()
|