oncall-engine/engine/apps/public_api/serializers/integrations.py
Vadim Stepanov e67d3519fe
Restore email notifications (#621)
* remove email verification related code

* remove email verification related code

* remove sendgrid callback

* remove sendgrid related code

* remove sendgrid related code

* rename sendgrid app to email

* remove email from built-in channels

* remove email from built-in channels

* remove email from built-in channels

* add email backend: https://github.com/grafana/oncall/pull/50

* add email templater

* add email templater

* convert md to html

* add email settings to live settings

* use task to send email, handle some exceptions to create logs

* remove ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED usage

* add email limit logic

* fix tests

* add docs

* remove old email templates

* remove old email templates

* add template_fields to messaging backend

* add messaging backends templates to public api

* add comment for deprecated fields

* fix test

* fix tests

* disable email by default

* don't retry on SMTPException and TimeoutError

* add tests

* bring email back to public api docs

* return ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED

* make template_fields tuple

* build_subject_and_title -> build_subject_and_message

* add one more comment about template deprecation

* use 8 as backend id

* add comment about gaierror and BadHeaderError

* add comment on importing in notify_user_async

* edit oss docs
2022-10-19 12:32:56 +01:00

241 lines
10 KiB
Python

from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from jinja2 import TemplateSyntaxError
from rest_framework import fields, serializers
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
from apps.alerts.models import AlertReceiveChannel
from apps.base.messaging import get_messaging_backends
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import NOTIFICATION_CHANNEL_OPTIONS, EagerLoadingMixin
from common.jinja_templater import jinja_template_env
from common.utils import timed_lru_cache
from .integtration_heartbeat import IntegrationHeartBeatSerializer
from .maintenance import MaintainableObjectSerializerMixin
from .routes import DefaultChannelFilterSerializer
class IntegrationTypeField(fields.CharField):
def to_representation(self, value):
return AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP[value]
def to_internal_value(self, data):
try:
integration_type = [
key for key, value in AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP.items() if value == data
][0]
except IndexError:
raise BadRequest(detail="Invalid integration type")
return integration_type
class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, MaintainableObjectSerializerMixin):
id = serializers.CharField(read_only=True, source="public_primary_key")
name = serializers.CharField(required=False, source="verbal_name")
team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team")
link = serializers.ReadOnlyField(source="integration_url")
type = IntegrationTypeField(source="integration")
templates = serializers.DictField(required=False)
default_route = serializers.DictField(required=False)
heartbeat = serializers.SerializerMethodField()
PREFETCH_RELATED = ["channel_filters"]
SELECT_RELATED = ["organization", "integration_heartbeat"]
class Meta:
model = AlertReceiveChannel
fields = MaintainableObjectSerializerMixin.Meta.fields + [
"id",
"name",
"team_id",
"link",
"type",
"default_route",
"templates",
"heartbeat",
]
def to_representation(self, instance):
result = super().to_representation(instance)
default_route = self._get_default_route_iterative(instance)
serializer = DefaultChannelFilterSerializer(default_route, context=self.context)
result["default_route"] = serializer.data
# add additional templates for messaging backends
result["templates"].update(self._get_messaging_backend_templates(instance))
return result
def create(self, validated_data):
validated_data = self._correct_validated_data(validated_data)
default_route_data = validated_data.pop("default_route", None)
organization = self.context["request"].auth.organization
integration = validated_data.get("integration")
if integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:
connection_error = GrafanaAlertingSyncManager.check_for_connection_errors(organization)
if connection_error:
raise serializers.ValidationError(connection_error)
with transaction.atomic():
instance = AlertReceiveChannel.create(
**validated_data,
author=self.context["request"].user,
organization=organization,
)
if default_route_data:
serializer = DefaultChannelFilterSerializer(
instance.default_channel_filter, default_route_data, context=self.context
)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
def validate(self, attrs):
organization = self.context["request"].auth.organization
verbal_name = attrs.get("verbal_name", None)
if verbal_name is None:
return attrs
try:
obj = AlertReceiveChannel.objects.get(organization=organization, verbal_name=verbal_name)
except AlertReceiveChannel.DoesNotExist:
return attrs
if self.instance and obj.id == self.instance.id:
return attrs
else:
raise BadRequest(detail="Integration with this name already exists")
def _correct_validated_data(self, validated_data):
validated_data = self._correct_validated_data_for_messaging_backends(validated_data)
templates = validated_data.pop("templates", {})
for template_name, templates_for_notification_channel in templates.items():
if type(templates_for_notification_channel) is dict:
for attr, template in templates_for_notification_channel.items():
try:
validated_data[AlertReceiveChannel.PUBLIC_TEMPLATES_FIELDS[template_name][attr]] = template
except KeyError:
raise BadRequest(detail="Invalid template data")
elif type(templates_for_notification_channel) is str:
try:
validated_data[
AlertReceiveChannel.PUBLIC_TEMPLATES_FIELDS[template_name]
] = templates_for_notification_channel
except KeyError:
raise BadRequest(detail="Invalid template data")
elif templates_for_notification_channel is None:
try:
template_to_set_to_default = AlertReceiveChannel.PUBLIC_TEMPLATES_FIELDS[template_name]
if type(template_to_set_to_default) is str:
validated_data[AlertReceiveChannel.PUBLIC_TEMPLATES_FIELDS[template_name]] = None
elif type(template_to_set_to_default) is dict:
for key in template_to_set_to_default.keys():
validated_data[AlertReceiveChannel.PUBLIC_TEMPLATES_FIELDS[template_name][key]] = None
except KeyError:
raise BadRequest(detail="Invalid template data")
return validated_data
def validate_templates(self, templates):
if not isinstance(templates, dict):
raise BadRequest(detail="Invalid template data")
for notification_channel in NOTIFICATION_CHANNEL_OPTIONS:
template_data = templates.get(notification_channel, {})
if template_data is None:
continue
if not isinstance(template_data, dict):
raise BadRequest(detail=f"Invalid {notification_channel} template data")
for attr, attr_template in template_data.items():
if attr_template is None:
continue
try:
jinja_template_env.from_string(attr_template)
except TemplateSyntaxError:
raise BadRequest(detail=f"invalid {notification_channel} {attr} template")
for common_template in ["resolve_signal", "grouping_key"]:
template_data = templates.get(common_template, "")
if template_data is None:
continue
if not isinstance(template_data, str):
raise BadRequest(detail=f"Invalid {common_template} template data")
try:
jinja_template_env.from_string(template_data)
except TemplateSyntaxError:
raise BadRequest(detail=f"Invalid {common_template} template data")
return templates
def _correct_validated_data_for_messaging_backends(self, validated_data):
templates = validated_data.get("templates", {})
messaging_backends_templates = self.instance.messaging_backends_templates if self.instance else None
for backend_id, backend in get_messaging_backends():
backend_templates = {}
if messaging_backends_templates is not None:
backend_templates = messaging_backends_templates.get(backend_id, {})
for field in backend.template_fields:
try:
template = templates[backend_id.lower()][field]
except KeyError:
continue
backend_templates[field] = template
# remove backend-specific template from payload
templates.pop(backend_id.lower(), None)
if backend_templates:
validated_data["messaging_backends_templates"] = messaging_backends_templates or {} | {
backend_id: backend_templates
}
return validated_data
@staticmethod
def _get_messaging_backend_templates(instance):
result = {}
messaging_backends_templates = instance.messaging_backends_templates or {}
for backend_id, backend in get_messaging_backends():
if not backend.template_fields:
continue
result[backend_id.lower()] = {
field: messaging_backends_templates.get(backend_id, {}).get(field) for field in backend.template_fields
}
return result
def get_heartbeat(self, obj):
try:
heartbeat = obj.integration_heartbeat
except ObjectDoesNotExist:
return None
return IntegrationHeartBeatSerializer(heartbeat).data
@timed_lru_cache(timeout=5)
def _get_default_route_iterative(self, obj):
"""
Gets default route iterative to not hit db on each integration instance.
"""
for filter in obj.channel_filters.all():
if filter.is_default:
return filter
class IntegrationUpdateSerializer(IntegrationSerializer):
type = IntegrationTypeField(source="integration", read_only=True)
team_id = TeamPrimaryKeyRelatedField(source="team", read_only=True)
def update(self, instance, validated_data):
validated_data = self._correct_validated_data(validated_data)
default_route_data = validated_data.pop("default_route", None)
default_route = instance.default_channel_filter
if default_route_data:
serializer = DefaultChannelFilterSerializer(default_route, default_route_data, context=self.context)
serializer.is_valid(raise_exception=True)
serializer.save()
return super().update(instance, validated_data)