oncall-engine/engine/apps/base/models/live_setting.py

248 lines
11 KiB
Python
Raw Normal View History

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",
2022-06-13 15:33:56 +03:00
"SLACK_INSTALL_RETURN_REDIRECT_HOST",
"SEND_ANONYMOUS_USAGE_STATS",
"GRAFANA_CLOUD_ONCALL_TOKEN",
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED",
2022-06-03 14:59:43 -03:00
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED",
"DANGEROUS_WEBHOOKS_ENABLED",
Phone provider refactoring (#1713) # 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>
2023-05-24 14:27:48 +08:00
"PHONE_PROVIDER",
"ZVONOK_API_KEY",
"ZVONOK_CAMPAIGN_ID",
"ZVONOK_AUDIO_ID",
"ZVONOK_SPEAKER_ID",
"ZVONOK_POSTBACK_CALL_ID",
"ZVONOK_POSTBACK_CAMPAIGN_ID",
"ZVONOK_POSTBACK_STATUS",
"ZVONOK_POSTBACK_USER_CHOICE",
"ZVONOK_POSTBACK_USER_CHOICE_ACK",
)
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 "
2022-06-13 15:33:56 +03:00
"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 "
2022-06-13 15:33:56 +03:00
"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 "
2022-06-13 15:33:56 +03:00
"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. "
2022-06-13 15:33:56 +03:00
"Slack secrets can't be verified on the backend, please try installing the Slack Bot "
"after you update them."
),
"TWILIO_ACCOUNT_SID": (
2022-10-12 11:01:17 +02:00
"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'>"
2022-10-12 11:01:17 +02:00
"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'>"
2022-10-12 11:01:17 +02:00
"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'>"
2022-10-12 11:01:17 +02:00
"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'>"
2022-10-12 11:01:17 +02:00
"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."
Merge dev to main (#69) * Log (failed) attempt to notify a user with viewer role * Remove https:// prefix from BASE_URL docker env var * Fix cloud heartbeat name * Polishing telegram * Update docker-compose.yml * Update plugin README (#48) * Update README and screenshot, remove plop for build info since version is now displayed prominently * Sign build Co-authored-by: Michael Derynck <michael.derynck@grafana.com> * Build actions (#38) * Drone, github action changes * Minor version updates * Update frontend dependencies * Re-enable unit test Co-authored-by: Michael Derynck <michael.derynck@grafana.com> * Revert stylelint version (#52) * Revert stylelint version * Build plugin as well as lint * Build in previous step Co-authored-by: Michael Derynck <michael.derynck@grafana.com> * Update screenshot (#53) Co-authored-by: Michael Derynck <michael.derynck@grafana.com> * oncall images for docs (#55) * Update README.md * Top menu fix * Fix db encoding * Add api key docs * Reverting utf8 fix * bug fixes * fix for link for OSS version * Fixing utf8 and docker compose * 8080 -> 8000 port for consistency * makeReq * Fixing images * Fixing port * Fixing port * Fixing port * Fixing port * Fixing port * Fixing port * Fixing port * Fixing port * Replace symlink with file for CHANGELOG.MD (#68) Co-authored-by: Michael Derynck <michael.derynck@grafana.com> Co-authored-by: Matias Bordese <mbordese@gmail.com> Co-authored-by: Matvey Kukuy <Matvey-Kuk@users.noreply.github.com> Co-authored-by: Innokentii Konstantinov <innokenty.konstantinov@grafana.com> Co-authored-by: Matvey Kukuy <matvey@amixr.io> Co-authored-by: Michael Derynck <michael.derynck@grafana.com> Co-authored-by: Alyssa Wada <101596687+alyssawada@users.noreply.github.com> Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
2022-06-14 09:14:45 -06:00
" 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.",
2022-06-25 11:19:40 +02:00
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED": "Enable heartbeat integration with Grafana Cloud OnCall.",
2022-06-03 14:59:43 -03:00
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED": "Enable SMS/call notifications via Grafana Cloud OnCall",
"DANGEROUS_WEBHOOKS_ENABLED": "Enable outgoing webhooks to private networks",
Phone provider refactoring (#1713) # 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>
2023-05-24 14:27:48 +08:00
"PHONE_PROVIDER": f"Phone provider name. Available options: {','.join(list(settings.PHONE_PROVIDERS.keys()))}",
"ZVONOK_API_KEY": "API public key. You can get it in Profile->Settings section.",
"ZVONOK_CAMPAIGN_ID": "Calls by API campaign ID. You can get it after campaign creation.",
"ZVONOK_AUDIO_ID": "Calls with specific audio. You can get it in Audioclips section.",
"ZVONOK_SPEAKER_ID": "Calls with speaker.",
"ZVONOK_POSTBACK_CALL_ID": "'Postback' call id (ct_call_id) query parameter name to validate a postback request.",
"ZVONOK_POSTBACK_CAMPAIGN_ID": "'Postback' company id (ct_campaign_id) query parameter name to validate a postback request.",
"ZVONOK_POSTBACK_STATUS": "'Postback' status (ct_status) query parameter name to validate a postback request.",
"ZVONOK_POSTBACK_USER_CHOICE": "'Postback' user choice (ct_user_choice) query parameter name (optional).",
"ZVONOK_POSTBACK_USER_CHOICE_ACK": "'Postback' user choice (ct_user_choice) query parameter value for acknowledge alert group (optional).",
}
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",
"ZVONOK_API_KEY",
)
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))
2022-11-01 18:24:44 -06:00
cls.validate_settings()
@classmethod
2022-11-01 18:24:44 -06:00
def validate_settings(cls):
settings_to_validate = cls.objects.all()
for setting in settings_to_validate:
setting.error = LiveSettingValidator(live_setting=setting).get_error()
setting.save(update_fields=["error"])
@staticmethod
def _get_setting_from_setting_file(setting_name):
return getattr(settings, setting_name)
def save(self, *args, **kwargs):
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}"
)
super().save(*args, **kwargs)