Make CloudConnection instance wide
This commit is contained in:
parent
4ef1ba9eb5
commit
829ed8230b
18 changed files with 128 additions and 98 deletions
|
|
@ -12,6 +12,7 @@ from apps.api.serializers.live_setting import LiveSettingSerializer
|
|||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.base.models import LiveSetting
|
||||
from apps.base.utils import live_settings
|
||||
from apps.oss_installation.models import CloudConnector
|
||||
from apps.slack.tasks import unpopulate_slack_user_identities
|
||||
from apps.telegram.client import TelegramClient
|
||||
from apps.telegram.tasks import register_telegram_webhook
|
||||
|
|
@ -66,6 +67,15 @@ class LiveSettingViewSet(PublicPrimaryKeyMixin, viewsets.ModelViewSet):
|
|||
if sti is not None:
|
||||
unpopulate_slack_user_identities.apply_async((sti.pk, True), countdown=0)
|
||||
|
||||
if instance.name == "GRAFANA_CLOUD_ONCALL_TOKEN":
|
||||
try:
|
||||
old_token = live_settings.GRAFANA_CLOUD_ONCALL_TOKEN
|
||||
except ImproperlyConfigured:
|
||||
old_token = None
|
||||
|
||||
if old_token != new_value:
|
||||
CloudConnector.remove_sync()
|
||||
|
||||
def _reset_telegram_integration(self, new_token):
|
||||
# tell Telegram to cancel sending events from old bot
|
||||
with suppress(ImproperlyConfigured, error.InvalidToken, error.Unauthorized):
|
||||
|
|
|
|||
|
|
@ -173,4 +173,5 @@ class LiveSetting(models.Model):
|
|||
)
|
||||
|
||||
self.error = LiveSettingValidator(live_setting=self).get_error()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import json
|
||||
import re
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests.exceptions
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from python_http_client import UnauthorizedError
|
||||
from sendgrid import SendGridAPIClient
|
||||
from telegram import Bot
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from twilio.rest import Client
|
||||
|
||||
from apps.oss_installation.models import CloudConnector
|
||||
|
||||
|
||||
class LiveSettingProxy:
|
||||
def __dir__(self):
|
||||
|
|
@ -98,18 +97,9 @@ class LiveSettingValidator:
|
|||
return f"Telegram error: {str(e)}"
|
||||
|
||||
@classmethod
|
||||
def _check_grafana_cloud_oncall_token(cls, grafan_oncall_token):
|
||||
try:
|
||||
info_url = urljoin(settings.GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/info/")
|
||||
r = requests.get(info_url, headers={"AUTHORIZATION": grafan_oncall_token}, timeout=5)
|
||||
if r.status_code == 200:
|
||||
return
|
||||
elif r.status_code == 403:
|
||||
return f"Invalid token"
|
||||
else:
|
||||
return f"Non-200 HTTP code. Got {r.status_code}"
|
||||
except requests.exceptions.RequestException as e:
|
||||
return f"Error {str(e)}"
|
||||
def _check_grafana_cloud_oncall_token(cls, grafana_oncall_token):
|
||||
_, err = CloudConnector.sync_with_cloud(grafana_oncall_token)
|
||||
return err
|
||||
|
||||
@staticmethod
|
||||
def _is_email_valid(email):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
CLOUD_URL = "https://a-prod-us-central-0.grafana.net/"
|
||||
|
||||
CLOUD_NOT_SYNCED = 0
|
||||
CLOUD_SYNCED_USER_NOT_FOUND = 1
|
||||
CLOUD_SYNCED_PHONE_NOT_VERIFIED = 2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .cloud_organization_connector import CloudOrganizationConnector # noqa: F401
|
||||
from .cloud_connector import CloudConnector # noqa: F401
|
||||
from .cloud_heartbeat import CloudHeartbeat # noqa: F401
|
||||
from .cloud_user_identity import CloudUserIdentity # noqa: F401
|
||||
from .heartbeat import CloudHeartbeat # noqa: F401
|
||||
from .oss_installation import OssInstallation # noqa: F401
|
||||
|
|
|
|||
|
|
@ -5,50 +5,53 @@ import requests
|
|||
from django.db import models, transaction
|
||||
|
||||
from apps.base.utils import live_settings
|
||||
from apps.oss_installation.constants import CLOUD_URL
|
||||
from apps.oss_installation.models import CloudHeartbeat
|
||||
from apps.oss_installation.models.cloud_user_identity import CloudUserIdentity
|
||||
from apps.user_management.models import User
|
||||
from settings.base import GRAFANA_CLOUD_ONCALL_API_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudOrganizationConnector(models.Model):
|
||||
class CloudConnector(models.Model):
|
||||
"""
|
||||
CloudOrganizationConnector model represents connection between oss organization and cloud organization.
|
||||
"""
|
||||
|
||||
cloud_url = models.URLField()
|
||||
organization = models.OneToOneField(
|
||||
"user_management.organization", related_name="cloud_connector", on_delete=models.CASCADE
|
||||
)
|
||||
# organization = models.OneToOneField(
|
||||
# "user_management.organization", related_name="cloud_connector", on_delete=models.CASCADE
|
||||
# )
|
||||
|
||||
@classmethod
|
||||
def sync_with_cloud(cls, organization):
|
||||
def sync_with_cloud(cls, token=None):
|
||||
"""
|
||||
sync_with_cloud sync organization with cloud organization defined by provided GRAFANA_CLOUD_ONCALL_TOKEN.
|
||||
"""
|
||||
sync_status = False
|
||||
error_msg = None
|
||||
|
||||
api_token = live_settings.GRAFANA_CLOUD_ONCALL_TOKEN
|
||||
api_token = token or live_settings.GRAFANA_CLOUD_ONCALL_TOKEN
|
||||
if api_token is None:
|
||||
logger.warning("Unable to sync with cloud. GRAFANA_CLOUD_ONCALL_TOKEN is not set")
|
||||
error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is not set"
|
||||
else:
|
||||
info_url = urljoin(CLOUD_URL, "api/v1/info/")
|
||||
info_url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/info/")
|
||||
try:
|
||||
r = requests.get(info_url, headers={"AUTHORIZATION": api_token}, timeout=5)
|
||||
if r.status_code == 200:
|
||||
cls.objects.update_or_create(organization=organization, defaults={"cloud_url": r.json()["url"]})
|
||||
sync_status = True
|
||||
connector = cls.objects.get_or_create()
|
||||
connector.cloud_url = r.json()["url"]
|
||||
connector.save()
|
||||
elif r.status_code == 403:
|
||||
logger.warning("Unable to sync with cloud. GRAFANA_CLOUD_ONCALL_TOKEN is invalid")
|
||||
error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is invalid"
|
||||
error_msg = "Invalid token"
|
||||
else:
|
||||
error_msg = f"Non-200 HTTP code. Got {r.status_code}"
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Unable to sync with cloud. Request exception {str(e)}")
|
||||
error_msg = f"Unable to sync with cloud"
|
||||
|
||||
return sync_status, error_msg
|
||||
|
||||
def sync_users_with_cloud(self) -> tuple[bool, str]:
|
||||
|
|
@ -60,9 +63,9 @@ class CloudOrganizationConnector(models.Model):
|
|||
logger.warning("Unable to sync with cloud. GRAFANA_CLOUD_ONCALL_TOKEN is not set")
|
||||
error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is not set"
|
||||
|
||||
existing_emails = list(User.objects.filter(organization=self.organization).values_list("email", flat=True))
|
||||
existing_emails = list(User.objects.values_list("email", flat=True))
|
||||
matching_users = []
|
||||
users_url = urljoin(CLOUD_URL, "api/v1/users")
|
||||
users_url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/users")
|
||||
|
||||
fetch_next_page = True
|
||||
users_fetched = True
|
||||
|
|
@ -98,11 +101,10 @@ class CloudOrganizationConnector(models.Model):
|
|||
cloud_id=user["id"],
|
||||
email=user["email"],
|
||||
phone_number_verified=user["is_phone_number_verified"],
|
||||
organization=self.organization,
|
||||
)
|
||||
)
|
||||
|
||||
CloudUserIdentity.objects.filter(organization=self.organization).delete()
|
||||
CloudUserIdentity.objects.delete()
|
||||
CloudUserIdentity.objects.bulk_create(cloud_users_identities_to_create, batch_size=1000)
|
||||
|
||||
return sync_status, error_msg
|
||||
|
|
@ -116,7 +118,7 @@ class CloudOrganizationConnector(models.Model):
|
|||
logger.warning(f"Unable to sync_user_with cloud user_id {user.id}. GRAFANA_CLOUD_ONCALL_TOKEN is not set")
|
||||
error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is not set"
|
||||
else:
|
||||
url = urljoin(CLOUD_URL, f"api/v1/users/?email={user.email}")
|
||||
url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, f"api/v1/users/?email={user.email}")
|
||||
try:
|
||||
r = requests.get(url, headers={"AUTHORIZATION": api_token}, timeout=5)
|
||||
if r.status_code != 200:
|
||||
|
|
@ -132,7 +134,6 @@ class CloudOrganizationConnector(models.Model):
|
|||
CloudUserIdentity.objects.filter(email=user.emai).delete()
|
||||
CloudUserIdentity.objects.create(
|
||||
email=user.email,
|
||||
organization=user.organization,
|
||||
phone_number_verified=cloud_used_data["is_phone_number_verified"],
|
||||
cloud_id=cloud_used_data["id"],
|
||||
)
|
||||
|
|
@ -147,3 +148,9 @@ class CloudOrganizationConnector(models.Model):
|
|||
error_msg = f"Unable to sync with cloud"
|
||||
|
||||
return sync_status, error_msg
|
||||
|
||||
@classmethod
|
||||
def remove_sync(cls):
|
||||
cls.objects.delete()
|
||||
CloudUserIdentity.objects.delete()
|
||||
CloudHeartbeat.objects.delete()
|
||||
|
|
@ -5,9 +5,9 @@ class CloudUserIdentity(models.Model):
|
|||
phone_number_verified = models.BooleanField(default=False)
|
||||
cloud_id = models.CharField(max_length=20)
|
||||
email = models.EmailField()
|
||||
organization = models.ForeignKey(
|
||||
"user_management.Organization", on_delete=models.CASCADE, related_name="cloud_users"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("email", "organization")
|
||||
# organization = models.ForeignKey(
|
||||
# "user_management.Organization", on_delete=models.CASCADE, related_name="cloud_users"
|
||||
# )
|
||||
#
|
||||
# class Meta:
|
||||
# unique_together = ("email", "organization")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from urllib.parse import urljoin
|
|||
from rest_framework import serializers
|
||||
|
||||
import apps.oss_installation.constants as cloud_constants
|
||||
from apps.oss_installation.models import CloudOrganizationConnector, CloudUserIdentity
|
||||
from apps.oss_installation.models import CloudConnector, CloudUserIdentity
|
||||
from apps.user_management.models import User
|
||||
|
||||
|
||||
|
|
@ -17,9 +17,7 @@ class CloudUserSerializer(serializers.ModelSerializer):
|
|||
def get_cloud_data(self, obj):
|
||||
link = None
|
||||
status = cloud_constants.CLOUD_NOT_SYNCED
|
||||
connector = CloudOrganizationConnector.objects.filter(
|
||||
organization=self.context["request"].auth.organization
|
||||
).first()
|
||||
connector = CloudConnector.objects.filter().first()
|
||||
if connector is not None:
|
||||
cloud_user_identity = CloudUserIdentity.objects.filter(email=obj.email).first()
|
||||
if cloud_user_identity is None:
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ from django.urls import path
|
|||
|
||||
from common.api_helpers.optional_slash_router import optional_slash_path
|
||||
|
||||
from .views import CloudConnectionStatusView, CloudHeartbeatStatusView, CloudUsersView, CloudUserView
|
||||
from .views import CloudConnectionView, CloudUsersView, CloudUserView
|
||||
|
||||
urlpatterns = [
|
||||
optional_slash_path("cloud_heartbeat_status", CloudHeartbeatStatusView.as_view(), name="cloud_heartbeat_status"),
|
||||
optional_slash_path("cloud_users", CloudUsersView.as_view(), name="cloud-users-list"),
|
||||
path(
|
||||
"cloud_users/<str:pk>",
|
||||
|
|
@ -16,5 +15,5 @@ urlpatterns = [
|
|||
),
|
||||
name="cloud-user-detail",
|
||||
),
|
||||
optional_slash_path("cloud_connection_status", CloudConnectionStatusView.as_view(), name="cloud-connection-status"),
|
||||
optional_slash_path("cloud_connection", CloudConnectionView.as_view(), name="cloud-connection-status"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
import logging
|
||||
from contextlib import suppress
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.apps import apps
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.alerts.models import AlertGroupLogRecord, EscalationPolicy
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
from apps.public_api.constants import DEMO_USER_ID
|
||||
from apps.schedules.ical_utils import list_users_to_notify_from_ical_for_period
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.user_management.models import User
|
||||
from settings.base import GRAFANA_CLOUD_ONCALL_API_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def active_oss_users_count():
|
||||
"""
|
||||
active_oss_users_count returns count of active users of oss installation.
|
||||
"""
|
||||
OnCallSchedule = apps.get_model("schedules", "OnCallSchedule")
|
||||
AlertGroupLogRecord = apps.get_model("alerts", "AlertGroupLogRecord")
|
||||
EscalationPolicy = apps.get_model("alerts", "EscalationPolicy")
|
||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||
User = apps.get_model("user_management", "User")
|
||||
|
||||
# Take logs for previous 24 hours
|
||||
start = timezone.now() - timezone.timedelta(hours=24)
|
||||
|
|
@ -68,3 +76,23 @@ def active_oss_users_count():
|
|||
with suppress(KeyError):
|
||||
unique_active_users.remove(demo_user.pk)
|
||||
return len(unique_active_users)
|
||||
|
||||
|
||||
def get_cloud_instance_info(api_token):
|
||||
success = False
|
||||
error_msg = None
|
||||
r = None
|
||||
info_url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/info/")
|
||||
try:
|
||||
r = requests.get(info_url, headers={"AUTHORIZATION": api_token}, timeout=5)
|
||||
if r.status_code == 200:
|
||||
success = True
|
||||
elif r.status_code == 403:
|
||||
logger.warning("Unable to sync with cloud. GRAFANA_CLOUD_ONCALL_TOKEN is invalid")
|
||||
error_msg = "Invalid token"
|
||||
else:
|
||||
error_msg = f"Non-200 HTTP code. Got {r.status_code}"
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Unable to sync with cloud. Request exception {str(e)}")
|
||||
error_msg = f"Unable to sync with cloud"
|
||||
return success, error_msg, r
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from .cloud_heartbeat_status import CloudHeartbeatStatusView # noqa: F401
|
||||
from .cloud_status import CloudConnectionStatusView # noqa: F401
|
||||
from .cloud_connection import CloudConnectionView # noqa: F401
|
||||
from .cloud_heartbeat import CloudHeartbeatStatusView # noqa: F401
|
||||
from .cloud_users import CloudUsersView, CloudUserView # noqa: F401
|
||||
|
|
|
|||
35
engine/apps/oss_installation/views/cloud_connection.py
Normal file
35
engine/apps/oss_installation/views/cloud_connection.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from urllib.parse import urljoin
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.api.permissions import IsAdmin
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.base.utils import live_settings
|
||||
from apps.oss_installation.models import CloudConnector, CloudHeartbeat
|
||||
|
||||
|
||||
class CloudConnectionView(APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, IsAdmin)
|
||||
|
||||
def get(self, request):
|
||||
connector = CloudConnector.objects.first()
|
||||
heartbeat = CloudHeartbeat.objects.first()
|
||||
response = {
|
||||
"cloud_connection_status": connector is not None,
|
||||
"token": live_settings.GRAFANA_CLOUD_ONCALL_TOKEN,
|
||||
"cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
|
||||
"cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED,
|
||||
"cloud_heartbeat_link": self._get_heartbeat_link(connector, heartbeat),
|
||||
"cloud_heartbeat_status": heartbeat is not None and heartbeat.success,
|
||||
}
|
||||
return Response(response)
|
||||
|
||||
def _get_heartbeat_link(self, connector, heartbeat):
|
||||
if connector is None:
|
||||
return None
|
||||
if heartbeat is None:
|
||||
return None
|
||||
return urljoin(connector.cloud_url, f"a/grafana-oncall-app/?page=integrations1&id={heartbeat.integration_id}")
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.oss_installation.models import CloudHeartbeat
|
||||
|
||||
|
||||
class CloudHeartbeatStatusView(APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request):
|
||||
response = {"status": CloudHeartbeat.status()}
|
||||
return Response(response)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.oss_installation.models import CloudOrganizationConnector
|
||||
|
||||
|
||||
class CloudConnectionStatusView(APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request):
|
||||
connector = CloudOrganizationConnector.objects.filter(organization=request.user.organization).first()
|
||||
|
||||
response = {
|
||||
"cloud_connection_status": connector is not None,
|
||||
}
|
||||
return Response(response)
|
||||
|
|
@ -9,7 +9,7 @@ from rest_framework.views import APIView
|
|||
import apps.oss_installation.constants as cloud_constants
|
||||
from apps.api.permissions import ActionPermission, IsAdmin, IsOwnerOrAdmin
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.oss_installation.models import CloudOrganizationConnector, CloudUserIdentity
|
||||
from apps.oss_installation.models import CloudConnector, CloudUserIdentity
|
||||
from apps.oss_installation.serializers import CloudUserSerializer
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin
|
||||
|
|
@ -31,12 +31,12 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
|
|||
results = self.paginate_queryset(queryset, request, view=self)
|
||||
|
||||
emails = list(queryset.values_list("email", flat=True))
|
||||
cloud_identities = list(CloudUserIdentity.objects.filter(organization=organization, email__in=emails))
|
||||
cloud_identities = list(CloudUserIdentity.objects.filter(email__in=emails))
|
||||
cloud_identities = {cloud_identity.email: cloud_identity for cloud_identity in cloud_identities}
|
||||
|
||||
response = []
|
||||
|
||||
connector = CloudOrganizationConnector.objects.filter(organization=organization)
|
||||
connector = CloudConnector.objects.first()
|
||||
|
||||
for user in results:
|
||||
link = None
|
||||
|
|
@ -65,9 +65,7 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
|
|||
return self.get_paginated_response(response)
|
||||
|
||||
def post(self, request):
|
||||
organization = request.user.organization
|
||||
|
||||
connector = CloudOrganizationConnector.objects.filter(organization=organization)
|
||||
connector = CloudConnector.objects.first()
|
||||
if connector is not None:
|
||||
sync_status, err = connector.sync_users_with_cloud()
|
||||
return Response(status=status.HTTP_200_OK, data={"status": sync_status, "error": err})
|
||||
|
|
@ -95,7 +93,7 @@ class CloudUserView(
|
|||
@action(detail=True, methods=["post"])
|
||||
def sync_with_cloud(self, request, pk):
|
||||
user = self.get_object()
|
||||
connector = CloudOrganizationConnector.objects.filter(organization=request["request"].auth.organization).first()
|
||||
connector = CloudConnector.objects.first()
|
||||
if connector is not None:
|
||||
sync_status, err = connector.sync_user_with_cloud(user)
|
||||
return Response(status=status.HTTP_200_OK, data={"status": sync_status, "error": err})
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
|||
path("slack/", include("apps.slack.urls")),
|
||||
]
|
||||
|
||||
if settings.OSS_INSTALLATION_FEATURES_ENABLED:
|
||||
if settings.OSS_INSTALLATION_FEATURES_ENABLED or True:
|
||||
urlpatterns += [
|
||||
path("api/internal/v1/", include("apps.oss_installation.urls")),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ SELF_HOSTED_SETTINGS = {
|
|||
"ORG_TITLE": "Self-Hosted Organization",
|
||||
}
|
||||
|
||||
GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get("GRAFANA_CLOUD_ONCALL_API_URL", "https://a-prod-us-central-0.grafana.net")
|
||||
GRAFANA_CLOUD_ONCALL_API_URL = "https://a-02-dev-us-central-0.grafana.net/"
|
||||
GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None)
|
||||
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
|
||||
GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", default=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue