Make CloudConnection instance wide

This commit is contained in:
Innokentii Konstantinov 2022-06-06 16:02:09 +04:00
parent 4ef1ba9eb5
commit 829ed8230b
18 changed files with 128 additions and 98 deletions

View file

@ -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):

View file

@ -173,4 +173,5 @@ class LiveSetting(models.Model):
)
self.error = LiveSettingValidator(live_setting=self).get_error()
super().save(*args, **kwargs)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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")

View file

@ -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:

View file

@ -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"),
]

View file

@ -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

View file

@ -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

View 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}")

View file

@ -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)

View file

@ -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)

View file

@ -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})

View file

@ -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")),
]

View file

@ -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)