oncall-engine/engine/apps/api/serializers/escalation_policy.py
Matias Bordese 2a89374adf
Add escalation chain support for new webhooks (#1654)
Allow setting a webhook as escalation chain policy step.
2023-04-05 12:03:55 +00:00

249 lines
9.1 KiB
Python

import time
from datetime import timedelta
from rest_framework import serializers
from apps.alerts.models import CustomButton, EscalationChain, EscalationPolicy
from apps.schedules.models import OnCallSchedule
from apps.slack.models import SlackUserGroup
from apps.user_management.models import User
from apps.webhooks.models import Webhook
from common.api_helpers.custom_fields import (
OrganizationFilteredPrimaryKeyRelatedField,
UsersFilteredByOrganizationField,
)
from common.api_helpers.mixins import EagerLoadingMixin
WAIT_DELAY = "wait_delay"
NOTIFY_SCHEDULE = "notify_schedule"
NOTIFY_TO_USERS_QUEUE = "notify_to_users_queue"
NOTIFY_GROUP = "notify_to_group"
FROM_TIME = "from_time"
TO_TIME = "to_time"
NUM_ALERTS_IN_WINDOW = "num_alerts_in_window"
NUM_MINUTES_IN_WINDOW = "num_minutes_in_window"
CUSTOM_BUTTON_TRIGGER = "custom_button_trigger"
CUSTOM_WEBHOOK_TRIGGER = "custom_webhook"
STEP_TYPE_TO_RELATED_FIELD_MAP = {
EscalationPolicy.STEP_WAIT: [WAIT_DELAY],
EscalationPolicy.STEP_NOTIFY_SCHEDULE: [NOTIFY_SCHEDULE],
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE: [NOTIFY_TO_USERS_QUEUE],
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS: [NOTIFY_TO_USERS_QUEUE],
EscalationPolicy.STEP_NOTIFY_GROUP: [NOTIFY_GROUP],
EscalationPolicy.STEP_NOTIFY_IF_TIME: [FROM_TIME, TO_TIME],
EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW: [NUM_ALERTS_IN_WINDOW, NUM_MINUTES_IN_WINDOW],
EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON: [CUSTOM_BUTTON_TRIGGER],
EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK: [CUSTOM_WEBHOOK_TRIGGER],
}
class EscalationPolicySerializer(EagerLoadingMixin, serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
escalation_chain = OrganizationFilteredPrimaryKeyRelatedField(queryset=EscalationChain.objects)
important = serializers.BooleanField(required=False)
notify_to_users_queue = UsersFilteredByOrganizationField(
queryset=User.objects,
required=False,
)
wait_delay = serializers.ChoiceField(
required=False,
choices=EscalationPolicy.WEB_DURATION_CHOICES,
allow_null=True,
)
num_minutes_in_window = serializers.ChoiceField(
required=False,
choices=EscalationPolicy.WEB_DURATION_CHOICES_MINUTES,
allow_null=True,
)
notify_schedule = OrganizationFilteredPrimaryKeyRelatedField(
queryset=OnCallSchedule.objects,
required=False,
allow_null=True,
)
notify_to_group = OrganizationFilteredPrimaryKeyRelatedField(
queryset=SlackUserGroup.objects,
required=False,
allow_null=True,
filter_field="slack_team_identity__organizations",
)
custom_button_trigger = OrganizationFilteredPrimaryKeyRelatedField(
queryset=CustomButton.objects,
required=False,
allow_null=True,
filter_field="organization",
)
custom_webhook = OrganizationFilteredPrimaryKeyRelatedField(
queryset=Webhook.objects,
required=False,
allow_null=True,
filter_field="organization",
)
class Meta:
model = EscalationPolicy
fields = [
"id",
"order",
"step",
"wait_delay",
"escalation_chain",
"notify_to_users_queue",
"from_time",
"to_time",
"num_alerts_in_window",
"num_minutes_in_window",
"slack_integration_required",
"custom_button_trigger",
"custom_webhook",
"notify_schedule",
"notify_to_group",
"important",
]
read_only_fields = ("order",)
SELECT_RELATED = [
"escalation_chain",
"notify_schedule",
"notify_to_group",
"custom_button_trigger",
"custom_webhook",
]
PREFETCH_RELATED = ["notify_to_users_queue"]
def validate(self, data):
fields_to_check = [
WAIT_DELAY,
NOTIFY_SCHEDULE,
NOTIFY_TO_USERS_QUEUE,
NOTIFY_GROUP,
FROM_TIME,
TO_TIME,
NUM_ALERTS_IN_WINDOW,
NUM_MINUTES_IN_WINDOW,
CUSTOM_BUTTON_TRIGGER,
CUSTOM_WEBHOOK_TRIGGER,
]
step = data.get("step")
if step is None:
raise serializers.ValidationError({"step": "This field is required."})
if data.get("important") and step in EscalationPolicy.STEPS_WITH_NO_IMPORTANT_VERSION_SET:
raise serializers.ValidationError(f"Step {step} can't be important")
for f in STEP_TYPE_TO_RELATED_FIELD_MAP.get(step, []):
fields_to_check.remove(f)
for field in fields_to_check:
if field == NOTIFY_TO_USERS_QUEUE:
# notify_to_users queue is m-to-m relation so we use empty list instead of None
if len(data.get(field, [])) != 0:
raise serializers.ValidationError(f"Invalid combination if step {step} and {field}")
else:
if data.get(field, None) is not None:
raise serializers.ValidationError(f"Invalid combination if step {step} and {field}")
return data
def validate_step(self, step_type):
organization = self.context["request"].user.organization
if step_type not in EscalationPolicy.INTERNAL_API_STEPS:
raise serializers.ValidationError("Invalid step value")
if step_type in EscalationPolicy.SLACK_INTEGRATION_REQUIRED_STEPS and organization.slack_team_identity is None:
raise serializers.ValidationError("Invalid escalation step type: step is Slack-specific")
return step_type
def to_internal_value(self, data):
data = self._wait_delay_to_internal_value(data)
return super().to_internal_value(data)
def to_representation(self, instance):
step = instance.step
result = super().to_representation(instance)
result = EscalationPolicySerializer._get_important_field(step, result)
return result
@staticmethod
def _wait_delay_to_internal_value(data):
if data.get(WAIT_DELAY, None):
try:
time.strptime(data[WAIT_DELAY], "%H:%M:%S")
except ValueError:
try:
data[WAIT_DELAY] = str(timedelta(seconds=float(data[WAIT_DELAY])))
except ValueError:
raise serializers.ValidationError("Invalid wait delay format")
return data
@staticmethod
def _get_important_field(step, result):
if step in {*EscalationPolicy.DEFAULT_STEPS_SET, *EscalationPolicy.STEPS_WITH_NO_IMPORTANT_VERSION_SET}:
result["important"] = False
elif step in EscalationPolicy.IMPORTANT_STEPS_SET:
result["important"] = True
result["step"] = EscalationPolicy.IMPORTANT_TO_DEFAULT_STEP_MAPPING[step]
return result
@staticmethod
def _convert_to_important_step_if_needed(validated_data):
step = validated_data.get("step")
important = validated_data.pop("important", None)
if step in EscalationPolicy.DEFAULT_STEPS_SET and important:
validated_data["step"] = EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING[step]
return validated_data
class EscalationPolicyCreateSerializer(EscalationPolicySerializer):
class Meta(EscalationPolicySerializer.Meta):
read_only_fields = ("order",)
extra_kwargs = {"escalation_chain": {"required": True, "allow_null": False}}
def create(self, validated_data):
validated_data = EscalationPolicyCreateSerializer._convert_to_important_step_if_needed(validated_data)
instance = super().create(validated_data)
return instance
class EscalationPolicyUpdateSerializer(EscalationPolicySerializer):
escalation_chain = serializers.CharField(read_only=True, source="escalation_chain.public_primary_key")
class Meta(EscalationPolicySerializer.Meta):
read_only_fields = ("order", "escalation_chain")
def update(self, instance, validated_data):
step = validated_data.get("step", instance.step)
validated_data = EscalationPolicyUpdateSerializer._drop_not_step_type_related_fields(step, validated_data)
validated_data = EscalationPolicyUpdateSerializer._convert_to_important_step_if_needed(validated_data)
return super().update(instance, validated_data)
@staticmethod
def _drop_not_step_type_related_fields(step, validated_data):
fields_to_set_none = [
WAIT_DELAY,
NOTIFY_SCHEDULE,
NOTIFY_TO_USERS_QUEUE,
NOTIFY_GROUP,
FROM_TIME,
TO_TIME,
NUM_ALERTS_IN_WINDOW,
NUM_MINUTES_IN_WINDOW,
CUSTOM_BUTTON_TRIGGER,
CUSTOM_WEBHOOK_TRIGGER,
]
for f in STEP_TYPE_TO_RELATED_FIELD_MAP.get(step, []):
fields_to_set_none.remove(f)
for f in fields_to_set_none:
if f == NOTIFY_TO_USERS_QUEUE:
# notify_to_users queue is m-to-m relation so we use empty list instead of None
validated_data[f] = []
else:
validated_data[f] = None
return validated_data