# What this PR does Add scheduled task to start cleanup tasks. Currently purpose is to run the task every 12 hours and for all active orgs cleanup empty & deleted integrations. For deleted orgs we can run this manually. It will also run if an org moves from active to deleted. It is expected to add more to cleanup_organization task over time. ## Which issue(s) this PR fixes ## 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)
206 lines
8.8 KiB
Python
206 lines
8.8 KiB
Python
import logging
|
|
import uuid
|
|
|
|
from celery.utils.log import get_task_logger
|
|
from django.conf import settings
|
|
from django.utils import timezone
|
|
|
|
from apps.grafana_plugin.helpers.client import GcomAPIClient, GrafanaAPIClient
|
|
from apps.user_management.models import Organization, Team, User
|
|
from apps.user_management.signals import org_sync_signal
|
|
from common.utils import task_lock
|
|
|
|
logger = get_task_logger(__name__)
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
def sync_organization(organization: Organization) -> None:
|
|
# ensure one sync task is running at most for a given org at a given time
|
|
lock_id = "sync-organization-lock-{}".format(organization.id)
|
|
random_value = str(uuid.uuid4())
|
|
with task_lock(lock_id, random_value) as acquired:
|
|
if acquired:
|
|
_sync_organization(organization)
|
|
else:
|
|
# sync already running
|
|
logger.info(f"Sync for Organization {organization.pk} already in progress.")
|
|
|
|
|
|
def _sync_organization(organization: Organization) -> None:
|
|
grafana_api_client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
|
|
|
|
# NOTE: checking whether or not RBAC is enabled depends on whether we are dealing with an open-source or cloud
|
|
# stack. For Cloud we should make a call to the GCOM API, using an admin API token, and get the list of
|
|
# feature_toggles enabled for the stack. For open-source, simply make a HEAD request to the grafana instance's API
|
|
# and consider RBAC enabled if the list RBAC permissions endpoint returns 200. We cannot simply rely on the HEAD
|
|
# call in cloud because if an instance is not active, the grafana gateway will still return 200 for the
|
|
# HEAD request.
|
|
if settings.LICENSE == settings.CLOUD_LICENSE_NAME:
|
|
gcom_client = GcomAPIClient(settings.GRAFANA_COM_ADMIN_API_TOKEN)
|
|
rbac_is_enabled = gcom_client.is_rbac_enabled_for_stack(organization.stack_id)
|
|
else:
|
|
rbac_is_enabled = grafana_api_client.is_rbac_enabled_for_organization()
|
|
|
|
organization.is_rbac_permissions_enabled = rbac_is_enabled
|
|
logger.info(f"RBAC status org={organization.pk} rbac_enabled={organization.is_rbac_permissions_enabled}")
|
|
|
|
_sync_instance_info(organization)
|
|
|
|
_, check_token_call_status = grafana_api_client.check_token()
|
|
if check_token_call_status["connected"]:
|
|
organization.api_token_status = Organization.API_TOKEN_STATUS_OK
|
|
sync_users_and_teams(grafana_api_client, organization)
|
|
organization.last_time_synced = timezone.now()
|
|
|
|
_sync_grafana_incident_plugin(organization, grafana_api_client)
|
|
_sync_grafana_labels_plugin(organization, grafana_api_client)
|
|
else:
|
|
organization.api_token_status = Organization.API_TOKEN_STATUS_FAILED
|
|
logger.warning(f"Sync not successful org={organization.pk} token_status=FAILED")
|
|
|
|
organization.save(
|
|
update_fields=[
|
|
"cluster_slug",
|
|
"stack_slug",
|
|
"org_slug",
|
|
"org_title",
|
|
"region_slug",
|
|
"grafana_url",
|
|
"last_time_synced",
|
|
"api_token_status",
|
|
"gcom_token_org_last_time_synced",
|
|
"is_rbac_permissions_enabled",
|
|
"is_grafana_incident_enabled",
|
|
"is_grafana_labels_enabled",
|
|
"grafana_incident_backend_url",
|
|
]
|
|
)
|
|
|
|
org_sync_signal.send(sender=None, organization=organization)
|
|
|
|
|
|
def _sync_instance_info(organization: Organization) -> None:
|
|
if organization.gcom_token:
|
|
gcom_client = GcomAPIClient(organization.gcom_token)
|
|
instance_info = gcom_client.get_instance_info(organization.stack_id)
|
|
|
|
if not instance_info or instance_info["orgId"] != organization.org_id:
|
|
return
|
|
|
|
organization.stack_slug = instance_info["slug"]
|
|
organization.org_slug = instance_info["orgSlug"]
|
|
organization.org_title = instance_info["orgName"]
|
|
organization.region_slug = instance_info["regionSlug"]
|
|
organization.grafana_url = instance_info["url"]
|
|
organization.cluster_slug = instance_info["clusterSlug"]
|
|
organization.gcom_token_org_last_time_synced = timezone.now()
|
|
|
|
|
|
def _sync_grafana_labels_plugin(organization: Organization, grafana_api_client) -> None:
|
|
"""
|
|
_sync_grafana_labels_plugin checks if grafana-labels-app plugin is enabled and sets a flag in the organization.
|
|
It intended to use only inside _sync_organization. It mutates, but not saves org, it's saved in _sync_organization.
|
|
"""
|
|
grafana_labels_plugin_settings, _ = grafana_api_client.get_grafana_labels_plugin_settings()
|
|
if grafana_labels_plugin_settings is not None:
|
|
organization.is_grafana_labels_enabled = grafana_labels_plugin_settings["enabled"]
|
|
|
|
|
|
def _sync_grafana_incident_plugin(organization: Organization, grafana_api_client) -> None:
|
|
"""
|
|
_sync_grafana_incident_plugin check if incident plugin is enabled and sets a flag and its url in the organization.
|
|
It intended to use only inside _sync_organization. It mutates, but not saves org, it's saved in _sync_organization.
|
|
"""
|
|
grafana_incident_settings, _ = grafana_api_client.get_grafana_incident_plugin_settings()
|
|
organization.is_grafana_incident_enabled = False
|
|
organization.grafana_incident_backend_url = None
|
|
|
|
if grafana_incident_settings is not None:
|
|
organization.is_grafana_incident_enabled = grafana_incident_settings["enabled"]
|
|
organization.grafana_incident_backend_url = (grafana_incident_settings.get("jsonData") or {}).get(
|
|
GrafanaAPIClient.GRAFANA_INCIDENT_PLUGIN_BACKEND_URL_KEY
|
|
)
|
|
|
|
|
|
def sync_users_and_teams(client: GrafanaAPIClient, organization: Organization) -> None:
|
|
sync_users(client, organization)
|
|
sync_teams(client, organization)
|
|
sync_team_members(client, organization)
|
|
|
|
|
|
def sync_users(client: GrafanaAPIClient, organization: Organization, **kwargs) -> None:
|
|
api_users = client.get_users(organization.is_rbac_permissions_enabled, **kwargs)
|
|
# check if api_users are shaped correctly. e.g. for paused instance, the response is not a list.
|
|
if not api_users or not isinstance(api_users, (tuple, list)):
|
|
return
|
|
User.objects.sync_for_organization(organization=organization, api_users=api_users)
|
|
|
|
|
|
def sync_teams(client: GrafanaAPIClient, organization: Organization, **kwargs) -> None:
|
|
api_teams_result, _ = client.get_teams(**kwargs)
|
|
if not api_teams_result:
|
|
return
|
|
api_teams = api_teams_result["teams"]
|
|
Team.objects.sync_for_organization(organization=organization, api_teams=api_teams)
|
|
|
|
|
|
def sync_team_members(client: GrafanaAPIClient, organization: Organization) -> None:
|
|
for team in organization.teams.all():
|
|
members, _ = client.get_team_members(team.team_id)
|
|
if not members:
|
|
continue
|
|
User.objects.sync_for_team(team=team, api_members=members)
|
|
|
|
|
|
def sync_users_for_teams(client: GrafanaAPIClient, organization: Organization, **kwargs) -> None:
|
|
api_teams_result, _ = client.get_teams(**kwargs)
|
|
if not api_teams_result:
|
|
return
|
|
api_teams = api_teams_result["teams"]
|
|
Team.objects.sync_for_organization(organization=organization, api_teams=api_teams)
|
|
|
|
|
|
def delete_organization_if_needed(organization: Organization) -> bool:
|
|
# Organization has a manually set API token, it will not be found within GCOM
|
|
# and would need to be deleted manually.
|
|
from apps.auth_token.models import PluginAuthToken
|
|
|
|
manually_provisioned_token = PluginAuthToken.objects.filter(organization_id=organization.pk).first()
|
|
if manually_provisioned_token:
|
|
logger.info(f"Organization {organization.pk} has PluginAuthToken. Probably it's needed to delete org manually.")
|
|
return False
|
|
|
|
# Use common token as organization.gcom_token could be already revoked
|
|
client = GcomAPIClient(settings.GRAFANA_COM_ADMIN_API_TOKEN)
|
|
is_stack_deleted = client.is_stack_deleted(organization.stack_id)
|
|
if not is_stack_deleted:
|
|
return False
|
|
|
|
organization.delete()
|
|
return True
|
|
|
|
|
|
def cleanup_organization(organization_pk: int) -> None:
|
|
logger.info(f"Start cleanup Organization {organization_pk}")
|
|
try:
|
|
organization = Organization.objects_with_deleted.get(pk=organization_pk)
|
|
|
|
from apps.grafana_plugin.tasks.sync import cleanup_empty_deleted_integrations
|
|
|
|
cleanup_empty_deleted_integrations.apply_async(
|
|
(
|
|
organization.pk,
|
|
False,
|
|
),
|
|
)
|
|
|
|
if delete_organization_if_needed(organization):
|
|
logger.info(
|
|
f"Deleting organization due to stack deletion. "
|
|
f"pk: {organization_pk}, stack_id: {organization.stack_id}, org_id: {organization.org_id}"
|
|
)
|
|
else:
|
|
logger.info(f"Organization {organization_pk} not deleted in gcom, no action taken")
|
|
|
|
except Organization.DoesNotExist:
|
|
logger.info(f"Organization {organization_pk} was not found")
|