# What this PR does This PR moves phone notification logic into separate object PhoneBackend and introduces PhoneProvider interface to hide actual implementation of external phone services provider. It should allow add new phone providers just by implementing one class (See SimplePhoneProvider for example). # Why [Asterisk PR](https://github.com/grafana/oncall/pull/1282) showed that our phone notification system is not flexible. However this is one of the most frequent community questions - how to add "X" phone provider. Also, this refactoring move us one step closer to unifying all notification backends, since with PhoneBackend all phone notification logic is collected in one place and independent from concrete realisation. # Highligts 1. PhoneBackend object - contains all phone notifications business logic. 2. PhoneProvider - interface to external phone services provider. 3. TwilioPhoneProvider and SimplePhoneProvider - two examples of PhoneProvider implementation. 4. PhoneCallRecord and SMSRecord models. I introduced these models to keep phone notification limits logic decoupled from external providers. Existing TwilioPhoneCall and TwilioSMS objects will be migrated to the new table to not to reset limits counter. To be able to receive status callbacks and gather from Twilio TwilioPhoneCall and TwilioSMS still exists, but they are linked to PhoneCallRecord and SMSRecord via fk, to not to leat twilio logic into core code. --------- Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
232 lines
9.9 KiB
Python
232 lines
9.9 KiB
Python
from django.conf import settings
|
|
from django.core.validators import MinLengthValidator
|
|
from django.db import models
|
|
from django.db.models import JSONField
|
|
|
|
from apps.base.utils import LiveSettingValidator
|
|
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
|
|
|
|
|
def generate_public_primary_key_for_live_setting():
|
|
prefix = "L"
|
|
new_public_primary_key = generate_public_primary_key(prefix)
|
|
|
|
failure_counter = 0
|
|
while LiveSetting.objects.filter(public_primary_key=new_public_primary_key).exists():
|
|
new_public_primary_key = increase_public_primary_key_length(
|
|
failure_counter=failure_counter, prefix=prefix, model_name="LiveSetting"
|
|
)
|
|
failure_counter += 1
|
|
|
|
return new_public_primary_key
|
|
|
|
|
|
class LiveSetting(models.Model):
|
|
public_primary_key = models.CharField(
|
|
max_length=20,
|
|
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
|
|
unique=True,
|
|
default=generate_public_primary_key_for_live_setting,
|
|
)
|
|
name = models.CharField(max_length=50, unique=True)
|
|
value = JSONField(null=True, default=None)
|
|
error = models.TextField(null=True, default=None)
|
|
|
|
AVAILABLE_NAMES = (
|
|
"EMAIL_HOST",
|
|
"EMAIL_PORT",
|
|
"EMAIL_HOST_USER",
|
|
"EMAIL_HOST_PASSWORD",
|
|
"EMAIL_USE_TLS",
|
|
"EMAIL_FROM_ADDRESS",
|
|
"INBOUND_EMAIL_ESP",
|
|
"INBOUND_EMAIL_DOMAIN",
|
|
"INBOUND_EMAIL_WEBHOOK_SECRET",
|
|
"TWILIO_ACCOUNT_SID",
|
|
"TWILIO_AUTH_TOKEN",
|
|
"TWILIO_API_KEY_SID",
|
|
"TWILIO_API_KEY_SECRET",
|
|
"TWILIO_NUMBER",
|
|
"TWILIO_VERIFY_SERVICE_SID",
|
|
"TELEGRAM_TOKEN",
|
|
"TELEGRAM_WEBHOOK_HOST",
|
|
"SLACK_CLIENT_OAUTH_ID",
|
|
"SLACK_CLIENT_OAUTH_SECRET",
|
|
"SLACK_SIGNING_SECRET",
|
|
"SLACK_INSTALL_RETURN_REDIRECT_HOST",
|
|
"SEND_ANONYMOUS_USAGE_STATS",
|
|
"GRAFANA_CLOUD_ONCALL_TOKEN",
|
|
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED",
|
|
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED",
|
|
"DANGEROUS_WEBHOOKS_ENABLED",
|
|
"PHONE_PROVIDER",
|
|
)
|
|
|
|
DESCRIPTIONS = {
|
|
"EMAIL_HOST": "SMTP server host. This email server will be used to notify users via email.",
|
|
"EMAIL_PORT": "SMTP server port",
|
|
"EMAIL_HOST_USER": "SMTP server user",
|
|
"EMAIL_HOST_PASSWORD": "SMTP server password",
|
|
"EMAIL_USE_TLS": "SMTP enable/disable TLS",
|
|
"EMAIL_FROM_ADDRESS": "Email address used to send emails. If not specified, EMAIL_HOST_USER will be used.",
|
|
"INBOUND_EMAIL_DOMAIN": "Inbound email domain",
|
|
"INBOUND_EMAIL_ESP": (
|
|
"Inbound email ESP name. "
|
|
"Available options: amazon_ses, mailgun, mailjet, mandrill, postal, postmark, sendgrid, sparkpost"
|
|
),
|
|
"INBOUND_EMAIL_WEBHOOK_SECRET": "Inbound email webhook secret",
|
|
"SLACK_SIGNING_SECRET": (
|
|
"Check <a href='"
|
|
"https://grafana.com/docs/oncall/latest/open-source/#slack-setup"
|
|
"' target='_blank'>instruction</a> for details how to set up Slack. "
|
|
"Slack secrets can't be verified on the backend, please try installing the Slack Bot "
|
|
"after you update them."
|
|
),
|
|
"SLACK_CLIENT_OAUTH_SECRET": (
|
|
"Check <a href='"
|
|
"https://grafana.com/docs/oncall/latest/open-source/#slack-setup"
|
|
"' target='_blank'>instruction</a> for details how to set up Slack. "
|
|
"Slack secrets can't be verified on the backend, please try installing the Slack Bot "
|
|
"after you update them."
|
|
),
|
|
"SLACK_CLIENT_OAUTH_ID": (
|
|
"Check <a href='"
|
|
"https://grafana.com/docs/oncall/latest/open-source/#slack-setup"
|
|
"' target='_blank'>instruction</a> for details how to set up Slack. "
|
|
"Slack secrets can't be verified on the backend, please try installing the Slack Bot "
|
|
"after you update them."
|
|
),
|
|
"SLACK_INSTALL_RETURN_REDIRECT_HOST": (
|
|
"Check <a href='"
|
|
"https://grafana.com/docs/oncall/latest/open-source/#slack-setup"
|
|
"' target='_blank'>instruction</a> for details how to set up Slack. "
|
|
"Slack secrets can't be verified on the backend, please try installing the Slack Bot "
|
|
"after you update them."
|
|
),
|
|
"TWILIO_ACCOUNT_SID": (
|
|
"Twilio account SID/username to allow OnCall to send SMSes and make phone calls, see "
|
|
"<a href='https://support.twilio.com/hc/en-us/articles/223136027-Auth-Tokens-and-How-to-Change-Them' target='_blank'>"
|
|
"here</a> for more info. Required."
|
|
),
|
|
"TWILIO_API_KEY_SID": (
|
|
"Twilio API key SID/username to allow OnCall to send SMSes and make phone calls, see "
|
|
"<a href='https://www.twilio.com/docs/iam/keys/api-key' target='_blank'>"
|
|
"here</a> for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required."
|
|
),
|
|
"TWILIO_API_KEY_SECRET": (
|
|
"Twilio API key secret/password to allow OnCall to send SMSes and make phone calls, see "
|
|
"<a href='https://www.twilio.com/docs/iam/keys/api-key' target='_blank'>"
|
|
"here</a> for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required."
|
|
),
|
|
"TWILIO_AUTH_TOKEN": (
|
|
"Twilio password to allow OnCall to send SMSes and make calls, see "
|
|
"<a href='https://support.twilio.com/hc/en-us/articles/223136027-Auth-Tokens-and-How-to-Change-Them' target='_blank'>"
|
|
"here</a> for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required."
|
|
),
|
|
"TWILIO_NUMBER": (
|
|
"Number from which you will receive calls and SMSes, "
|
|
"<a href='https://www.twilio.com/docs/phone-numbers' target='_blank'>more info</a>."
|
|
),
|
|
"TWILIO_VERIFY_SERVICE_SID": (
|
|
"SID of Twilio service for number verification. "
|
|
"You can create a service in Twilio web interface. "
|
|
"twilio.com -> verify -> create new service."
|
|
),
|
|
"TELEGRAM_TOKEN": (
|
|
"Secret token for Telegram bot, you can get one via <a href='https://t.me/BotFather' target='_blank'>BotFather</a>."
|
|
),
|
|
"TELEGRAM_WEBHOOK_HOST": (
|
|
"Externally available URL for Telegram to make requests. Must use https and ports 80, 88, 443, 8443."
|
|
),
|
|
"SEND_ANONYMOUS_USAGE_STATS": (
|
|
"Grafana OnCall will send anonymous, but uniquely-identifiable usage analytics to Grafana Labs."
|
|
" These statistics are sent to https://stats.grafana.org/. For more information on what's sent, look at the "
|
|
"<a href='https://github.com/grafana/oncall/blob/dev/engine/apps/oss_installation/usage_stats.py#L29' target='_blank'> source code</a>."
|
|
),
|
|
"GRAFANA_CLOUD_ONCALL_TOKEN": "Secret token for Grafana Cloud OnCall instance.",
|
|
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED": "Enable heartbeat integration with Grafana Cloud OnCall.",
|
|
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED": "Enable SMS/call notifications via Grafana Cloud OnCall",
|
|
"DANGEROUS_WEBHOOKS_ENABLED": "Enable outgoing webhooks to private networks",
|
|
"PHONE_PROVIDER": f"Phone provider name. Available options: {','.join(list(settings.PHONE_PROVIDERS.keys()))}",
|
|
}
|
|
|
|
SECRET_SETTING_NAMES = (
|
|
"EMAIL_HOST_PASSWORD",
|
|
"INBOUND_EMAIL_WEBHOOK_SECRET",
|
|
"TWILIO_ACCOUNT_SID",
|
|
"TWILIO_AUTH_TOKEN",
|
|
"TWILIO_API_KEY_SID",
|
|
"TWILIO_API_KEY_SECRET",
|
|
"TWILIO_VERIFY_SERVICE_SID",
|
|
"SLACK_CLIENT_OAUTH_ID",
|
|
"SLACK_CLIENT_OAUTH_SECRET",
|
|
"SLACK_SIGNING_SECRET",
|
|
"TELEGRAM_TOKEN",
|
|
"GRAFANA_CLOUD_ONCALL_TOKEN",
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
@property
|
|
def description(self):
|
|
return self.DESCRIPTIONS.get(self.name)
|
|
|
|
@property
|
|
def default_value(self):
|
|
return self._get_setting_from_setting_file(self.name)
|
|
|
|
@property
|
|
def is_secret(self):
|
|
return self.name in self.SECRET_SETTING_NAMES
|
|
|
|
@classmethod
|
|
def get_setting(cls, setting_name):
|
|
if not settings.FEATURE_LIVE_SETTINGS_ENABLED:
|
|
return cls._get_setting_from_setting_file(setting_name)
|
|
|
|
if setting_name not in cls.AVAILABLE_NAMES:
|
|
raise ValueError(
|
|
f"Setting with name '{setting_name}' is not in list of available names {cls.AVAILABLE_NAMES}"
|
|
)
|
|
|
|
live_setting = cls.objects.filter(name=setting_name).first()
|
|
if live_setting is not None:
|
|
return live_setting.value
|
|
else:
|
|
return cls._get_setting_from_setting_file(setting_name)
|
|
|
|
@classmethod
|
|
def populate_settings_if_needed(cls):
|
|
settings_in_db = cls.objects.filter(name__in=cls.AVAILABLE_NAMES).values_list("name", flat=True)
|
|
setting_names_to_populate = set(cls.AVAILABLE_NAMES) - set(settings_in_db)
|
|
if len(setting_names_to_populate) == 0:
|
|
return
|
|
|
|
for setting_name in setting_names_to_populate:
|
|
cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name))
|
|
|
|
cls.validate_settings()
|
|
|
|
@classmethod
|
|
def validate_settings(cls):
|
|
settings_to_validate = cls.objects.all()
|
|
for setting in settings_to_validate:
|
|
setting.save(update_fields=["error"])
|
|
|
|
@staticmethod
|
|
def _get_setting_from_setting_file(setting_name):
|
|
return getattr(settings, setting_name)
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Save validates LiveSettings values and save them in database
|
|
"""
|
|
if self.name not in self.AVAILABLE_NAMES:
|
|
raise ValueError(
|
|
f"Setting with name '{self.name}' is not in list of available names {self.AVAILABLE_NAMES}"
|
|
)
|
|
|
|
self.error = LiveSettingValidator(live_setting=self).get_error()
|
|
|
|
super().save(*args, **kwargs)
|