oncall-engine/engine/apps/base/utils.py

197 lines
6.7 KiB
Python
Raw Permalink Normal View History

import json
import re
import typing
from urllib.parse import urlparse
import phonenumbers
from django.conf import settings
2022-09-21 12:36:52 -06:00
from phonenumbers import NumberParseException
from telegram import Bot
from twilio.base.exceptions import TwilioException
from twilio.rest import Client
from common.api_helpers.utils import create_engine_url
class LiveSettingProxy:
def __dir__(self):
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-25 10:43:23 +01:00
from apps.base.models import LiveSetting
return LiveSetting.AVAILABLE_NAMES
def __getattr__(self, item):
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-25 10:43:23 +01:00
from apps.base.models import LiveSetting
value = LiveSetting.get_setting(item)
return value
def __setattr__(self, key, value):
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-25 10:43:23 +01:00
from apps.base.models import LiveSetting
LiveSetting.objects.update_or_create(name=key, defaults={"value": value})
live_settings = LiveSettingProxy()
class LiveSettingValidator:
EMPTY_VALID_NAMES = (
"TWILIO_AUTH_TOKEN",
"TWILIO_API_KEY_SID",
"TWILIO_API_KEY_SECRET",
)
EMAIL_SSL_TLS_ERROR_MSG = "Cannot set Email (SMTP) to use SSL and TLS at the same time"
def __init__(self, live_setting):
self.live_setting = live_setting
def get_error(self):
check_fn_name = f"_check_{self.live_setting.name.lower()}"
if self.live_setting.value in (None, "") and self.live_setting.name not in self.EMPTY_VALID_NAMES:
return "Empty"
# skip validation if there's no handler for it
if not hasattr(self, check_fn_name):
return None
check_fn = getattr(self, check_fn_name)
return check_fn(self.live_setting.value)
@classmethod
def _check_twilio_api_key_sid(cls, twilio_api_key_sid):
if live_settings.TWILIO_AUTH_TOKEN:
return
try:
Client(
twilio_api_key_sid, live_settings.TWILIO_API_KEY_SECRET, live_settings.TWILIO_ACCOUNT_SID
).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_api_key_secret(cls, twilio_api_key_secret):
if live_settings.TWILIO_AUTH_TOKEN:
return
try:
Client(
live_settings.TWILIO_API_KEY_SID, twilio_api_key_secret, live_settings.TWILIO_ACCOUNT_SID
).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_account_sid(cls, twilio_account_sid):
try:
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
Client(
live_settings.TWILIO_API_KEY_SID, live_settings.TWILIO_API_KEY_SECRET, twilio_account_sid
).api.applications.list(limit=1)
else:
Client(twilio_account_sid, live_settings.TWILIO_AUTH_TOKEN).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_auth_token(cls, twilio_auth_token):
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
return
try:
Client(live_settings.TWILIO_ACCOUNT_SID, twilio_auth_token).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_verify_service_sid(cls, twilio_verify_service_sid):
try:
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
twilio_client = Client(
live_settings.TWILIO_API_KEY_SID,
live_settings.TWILIO_API_KEY_SECRET,
live_settings.TWILIO_ACCOUNT_SID,
)
else:
twilio_client = Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN)
twilio_client.verify.services(twilio_verify_service_sid).rate_limits.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_number(cls, twilio_number):
if not cls._is_phone_number_valid(twilio_number):
return "Please specify a valid phone number in the following format: +XXXXXXXXXXX"
@classmethod
def _check_slack_install_return_redirect_host(cls, slack_install_return_redirect_host):
scheme = urlparse(slack_install_return_redirect_host).scheme
if scheme != "https":
return "Must use https"
@classmethod
def _check_telegram_token(cls, telegram_token):
try:
bot = Bot(telegram_token)
bot.get_me()
except Exception as e:
return f"Telegram error: {str(e)}"
@classmethod
def _check_telegram_webhook_host(cls, telegram_webhook_host):
if settings.FEATURE_TELEGRAM_LONG_POLLING_ENABLED:
return
try:
# avoid circular import
from apps.telegram.client import TelegramClient
url = create_engine_url("/telegram/", override_base=telegram_webhook_host)
TelegramClient().register_webhook(url)
except Exception as e:
return f"Telegram error: {str(e)}"
2022-06-04 16:49:10 +04:00
@classmethod
2022-06-06 16:02:09 +04:00
def _check_grafana_cloud_oncall_token(cls, grafana_oncall_token):
2022-06-06 16:36:49 +04:00
from apps.oss_installation.models import CloudConnector
2022-06-06 16:02:09 +04:00
_, err = CloudConnector.sync_with_cloud(grafana_oncall_token)
return err
2022-06-04 16:49:10 +04:00
@classmethod
def _check_email_use_tls(cls, email_use_tls: bool) -> typing.Optional[str]:
return cls.EMAIL_SSL_TLS_ERROR_MSG if live_settings.EMAIL_USE_SSL is True and email_use_tls is True else None
@classmethod
def _check_email_use_ssl(cls, email_use_ssl: bool) -> typing.Optional[str]:
return cls.EMAIL_SSL_TLS_ERROR_MSG if live_settings.EMAIL_USE_TLS is True and email_use_ssl is True else None
@staticmethod
def _is_email_valid(email):
return re.match(r"^[^@]+@[^@]+\.[^@]+$", email)
@staticmethod
def _is_phone_number_valid(phone_number):
2022-09-21 12:36:52 -06:00
try:
ph_num = phonenumbers.parse(phone_number)
return phonenumbers.is_valid_number(ph_num)
except NumberParseException:
return False
@staticmethod
def _prettify_twilio_error(exc):
if isinstance(exc, TwilioException):
if len(exc.args) > 1:
response_content = exc.args[1].content
content = json.loads(response_content)
error_code = content["code"]
more_info = content["more_info"]
return f"Twilio error: code {error_code}. Learn more: {more_info}"
else:
return f"Twilio error: {exc.args[0]}"
else:
return f"Twilio error: {str(exc)}"