diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 100b5b7b7e..86d31610ee 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -617,7 +617,7 @@ def get_verbal(self): def force_disable_maintenance(self, user): disable_maintenance(alert_receive_channel_id=self.pk, force=True, user_id=user.pk) - def notify_about_maintenance_action(self, text, send_to_general_log_channel=True): + def notify_about_maintenance_action(self, text: str, send_to_general_log_channel=True) -> None: # TODO: this method should be refactored. # It's binded to slack and sending maintenance notification only there. channel_ids = list( @@ -627,7 +627,7 @@ def notify_about_maintenance_action(self, text, send_to_general_log_channel=True ) if send_to_general_log_channel: - general_log_channel_id = self.organization.general_log_channel_id + general_log_channel_id = self.organization.default_slack_channel_slack_id if general_log_channel_id is not None: channel_ids.append(general_log_channel_id) unique_channels_id = set(channel_ids) diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index 969f6c66b5..bb817a7989 100644 --- a/engine/apps/alerts/models/channel_filter.py +++ b/engine/apps/alerts/models/channel_filter.py @@ -45,6 +45,7 @@ class ChannelFilter(OrderedModel): """ alert_groups: "RelatedManager['AlertGroup']" + alert_receive_channel: "AlertReceiveChannel" filtering_labels: typing.Optional[list["LabelPair"]] order_with_respect_to = ["alert_receive_channel_id", "is_default"] @@ -68,6 +69,14 @@ class ChannelFilter(OrderedModel): notify_in_telegram = models.BooleanField(null=True, default=False) slack_channel_id = models.CharField(max_length=100, null=True, default=None) + # TODO: migrate slack_channel_id to slack_channel + # slack_channel = models.ForeignKey( + # 'slack.SlackChannel', + # null=True, + # default=None, + # on_delete=models.SET_NULL, + # related_name='+', + # ) telegram_channel = models.ForeignKey( "telegram.TelegramToOrganizationConnector", @@ -167,7 +176,7 @@ def slack_channel_id_or_general_log_id(self): if slack_team_identity is None: return None if self.slack_channel_id is None: - return organization.general_log_channel_id + return organization.default_slack_channel_slack_id else: return self.slack_channel_id diff --git a/engine/apps/alerts/models/resolution_note.py b/engine/apps/alerts/models/resolution_note.py index 1624cb658f..50f114df55 100644 --- a/engine/apps/alerts/models/resolution_note.py +++ b/engine/apps/alerts/models/resolution_note.py @@ -73,7 +73,17 @@ class ResolutionNoteSlackMessage(models.Model): related_name="added_resolution_note_slack_messages", ) text = models.TextField(max_length=3000, default=None, null=True) + slack_channel_id = models.CharField(max_length=100, null=True, default=None) + # TODO: migrate slack_channel_id to slack_channel + # slack_channel = models.ForeignKey( + # 'slack.SlackChannel', + # null=True, + # default=None, + # on_delete=models.SET_NULL, + # related_name='+', + # ) + ts = models.CharField(max_length=100, null=True, default=None) thread_ts = models.CharField(max_length=100, null=True, default=None) permalink = models.CharField(max_length=250, null=True, default=None) diff --git a/engine/apps/alerts/tests/test_alert_receiver_channel.py b/engine/apps/alerts/tests/test_alert_receiver_channel.py index 282b8c18b1..d690cd2fb9 100644 --- a/engine/apps/alerts/tests/test_alert_receiver_channel.py +++ b/engine/apps/alerts/tests/test_alert_receiver_channel.py @@ -167,7 +167,7 @@ def test_send_demo_alert_not_enabled(mocked_create_alert, make_organization, mak @pytest.mark.django_db def test_notify_maintenance_no_general_channel(make_organization, make_alert_receive_channel): - organization = make_organization(general_log_channel_id=None) + organization = make_organization(default_slack_channel=None) alert_receive_channel = make_alert_receive_channel(organization) with patch("apps.alerts.models.alert_receive_channel.post_message_to_channel") as mock_post_message: @@ -177,21 +177,34 @@ def test_notify_maintenance_no_general_channel(make_organization, make_alert_rec @pytest.mark.django_db -def test_notify_maintenance_with_general_channel(make_organization, make_alert_receive_channel): - organization = make_organization(general_log_channel_id="CHANNEL-ID") +def test_notify_maintenance_with_general_channel( + make_organization, + make_alert_receive_channel, + make_slack_team_identity, + make_slack_channel, +): + slack_channel = make_slack_channel(make_slack_team_identity()) + organization = make_organization(default_slack_channel=slack_channel) alert_receive_channel = make_alert_receive_channel(organization) with patch("apps.alerts.models.alert_receive_channel.post_message_to_channel") as mock_post_message: alert_receive_channel.notify_about_maintenance_action("maintenance mode enabled") mock_post_message.assert_called_once_with( - organization, organization.general_log_channel_id, "maintenance mode enabled" + organization, organization.default_slack_channel.slack_id, "maintenance mode enabled" ) @pytest.mark.django_db -def test_get_or_create_manual_integration_deleted_team(make_organization, make_team, make_alert_receive_channel): - organization = make_organization(general_log_channel_id="CHANNEL-ID") +def test_get_or_create_manual_integration_deleted_team( + make_organization, + make_team, + make_slack_team_identity, + make_slack_channel, +): + slack_channel = make_slack_channel(make_slack_team_identity()) + organization = make_organization(default_slack_channel=slack_channel) + # setup general manual integration general_manual = AlertReceiveChannel.get_or_create_manual_integration( organization=organization, team=None, integration=AlertReceiveChannel.INTEGRATION_MANUAL, defaults={} diff --git a/engine/apps/api/serializers/organization.py b/engine/apps/api/serializers/organization.py index 124b51b78c..28f7ea8a9b 100644 --- a/engine/apps/api/serializers/organization.py +++ b/engine/apps/api/serializers/organization.py @@ -2,6 +2,7 @@ from rest_framework import serializers +from apps.api.serializers.slack_channel import SlackChannelSerializer from apps.base.messaging import get_messaging_backend_from_id from apps.base.models import LiveSetting from apps.phone_notifications.phone_provider import get_phone_provider @@ -21,7 +22,7 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer): slack_team_identity = FastSlackTeamIdentitySerializer(read_only=True) name = serializers.CharField(required=False, allow_null=True, allow_blank=True, source="org_title") - slack_channel = serializers.SerializerMethodField() + slack_channel = SlackChannelSerializer(read_only=True, allow_null=True, required=False) rbac_enabled = serializers.BooleanField(read_only=True, source="is_rbac_permissions_enabled") grafana_incident_enabled = serializers.BooleanField(read_only=True, source="is_grafana_incident_enabled") @@ -47,22 +48,6 @@ class Meta: "grafana_incident_enabled", ] - def get_slack_channel(self, obj): - from apps.slack.models import SlackChannel - - if obj.general_log_channel_id is None or obj.slack_team_identity is None: - return None - try: - channel = obj.slack_team_identity.get_cached_channels().get(slack_id=obj.general_log_channel_id) - except SlackChannel.DoesNotExist: - return {"display_name": None, "slack_id": obj.general_log_channel_id, "id": None} - - return { - "display_name": channel.name, - "slack_id": channel.slack_id, - "id": channel.public_primary_key, - } - class CurrentOrganizationSerializer(OrganizationSerializer): env_status = serializers.SerializerMethodField() diff --git a/engine/apps/api/tests/test_set_general_log_channel.py b/engine/apps/api/tests/test_set_org_default_slack_channel.py similarity index 80% rename from engine/apps/api/tests/test_set_general_log_channel.py rename to engine/apps/api/tests/test_set_org_default_slack_channel.py index ad6a708efa..59b0ce9e8a 100644 --- a/engine/apps/api/tests/test_set_general_log_channel.py +++ b/engine/apps/api/tests/test_set_org_default_slack_channel.py @@ -20,7 +20,7 @@ (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) -def test_set_general_log_channel_permissions( +def test_set_org_default_slack_channel_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, @@ -29,8 +29,10 @@ def test_set_general_log_channel_permissions( _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() - url = reverse("api-internal:api-set-general-log-channel") - with patch("apps.api.views.organization.SetGeneralChannel.post", return_value=Response(status=status.HTTP_200_OK)): + url = reverse("api-internal:set-default-slack-channel") + with patch( + "apps.api.views.organization.SetDefaultSlackChannel.post", return_value=Response(status=status.HTTP_200_OK) + ): response = client.post(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index 5be32db025..46999cee8b 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -22,7 +22,7 @@ GetChannelVerificationCode, GetTelegramVerificationCode, OrganizationConfigChecksView, - SetGeneralChannel, + SetDefaultSlackChannel, ) from .views.preview_template_options import PreviewTemplateOptionsView from .views.public_api_tokens import PublicApiTokenView @@ -71,7 +71,7 @@ urlpatterns = [ path("", include(router.urls)), optional_slash_path("user", CurrentUserView.as_view(), name="api-user"), - optional_slash_path("set_general_channel", SetGeneralChannel.as_view(), name="api-set-general-log-channel"), + optional_slash_path("set_general_channel", SetDefaultSlackChannel.as_view(), name="set-default-slack-channel"), optional_slash_path("organization", CurrentOrganizationView.as_view(), name="api-organization"), optional_slash_path( "organization/config-checks", diff --git a/engine/apps/api/views/organization.py b/engine/apps/api/views/organization.py index 8b6f5d70b1..ee2b22e77d 100644 --- a/engine/apps/api/views/organization.py +++ b/engine/apps/api/views/organization.py @@ -108,7 +108,7 @@ def get(self, request): return Response(code) -class SetGeneralChannel(APIView): +class SetDefaultSlackChannel(APIView): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, RBACPermission) @@ -127,6 +127,6 @@ def post(self, request): public_primary_key=slack_channel_id, slack_team_identity=slack_team_identity ) - organization.set_general_log_channel(slack_channel.slack_id, slack_channel.name, request.user) + organization.set_default_slack_channel(slack_channel, request.user) return Response(status=200) diff --git a/engine/apps/integrations/tasks.py b/engine/apps/integrations/tasks.py index 0ee5acbb82..45f3e04f2a 100644 --- a/engine/apps/integrations/tasks.py +++ b/engine/apps/integrations/tasks.py @@ -169,9 +169,17 @@ def notify_about_integration_ratelimit_in_slack(organization_id, text, **kwargs) else: cache.set(cache_key, True, 60 * 15) # Set cache before sending message to make sure we don't ratelimit slack slack_team_identity = organization.slack_team_identity - if slack_team_identity is not None: + org_default_slack_channel_id = organization.default_slack_channel_slack_id + + if slack_team_identity is not None and org_default_slack_channel_id is not None: try: sc = SlackClient(slack_team_identity, enable_ratelimit_retry=True) - sc.chat_postMessage(channel=organization.general_log_channel_id, text=text) + sc.chat_postMessage(channel=org_default_slack_channel_id, text=text) except SlackAPIError as e: logger.warning(f"Slack exception {e} while sending message for organization {organization_id}") + else: + logger.info( + f"Slack team identity or general log channel is not set for organization {organization_id} " + f"skipping rest of notify_about_integration_ratelimit_in_slack " + f"slack_team_identity={slack_team_identity} org_default_slack_channel_id={org_default_slack_channel_id}" + ) diff --git a/engine/apps/slack/models/slack_team_identity.py b/engine/apps/slack/models/slack_team_identity.py index ee73c006bb..d46cc7d230 100644 --- a/engine/apps/slack/models/slack_team_identity.py +++ b/engine/apps/slack/models/slack_team_identity.py @@ -19,10 +19,13 @@ if typing.TYPE_CHECKING: from django.db.models.manager import RelatedManager + from apps.slack.models import SlackChannel + logger = logging.getLogger(__name__) class SlackTeamIdentity(models.Model): + cached_channels: "RelatedManager['SlackChannel']" organizations: "RelatedManager[Organization]" id = models.AutoField(primary_key=True) diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index 6bac3db91a..218134b18d 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -71,7 +71,7 @@ def process_signal(self, alert: Alert) -> None: alert.group.channel_filter.slack_channel_id_or_general_log_id if alert.group.channel_filter # if channel filter is deleted mid escalation, use default Slack channel - else alert.group.channel.organization.general_log_channel_id + else alert.group.channel.organization.default_slack_channel_slack_id ) self._send_first_alert(alert, channel_id) except (SlackAPIError, TimeoutError): diff --git a/engine/apps/slack/tasks.py b/engine/apps/slack/tasks.py index 5831f84e79..37960bc52d 100644 --- a/engine/apps/slack/tasks.py +++ b/engine/apps/slack/tasks.py @@ -198,8 +198,8 @@ def unpopulate_slack_user_identities(organization_pk, force=False, ts=None): if force: organization.slack_team_identity = None - organization.general_log_channel_id = None - organization.save(update_fields=["slack_team_identity", "general_log_channel_id"]) + organization.default_slack_channel = None + organization.save(update_fields=["slack_team_identity", "default_slack_channel"]) @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=0) @@ -555,11 +555,14 @@ def clean_slack_integration_leftovers(organization_id, *args, **kwargs): @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=10) def clean_slack_channel_leftovers(slack_team_identity_id, slack_channel_id): """ - This task removes binding to slack channel after channel arcived or deleted in slack. + TODO: once we add/migrate to ChannelFilter.slack_channel, this will mean that we no longer need this task + and it can be safely removed (foreign key relationships to a slack channel that is deleted in the db will + automatically be set to None due to on_delete=models.SET_NULL) + + This task removes binding to slack channel after channel archived or deleted in slack. """ from apps.alerts.models import ChannelFilter from apps.slack.models import SlackTeamIdentity - from apps.user_management.models import Organization try: sti = SlackTeamIdentity.objects.get(id=slack_team_identity_id) @@ -569,16 +572,7 @@ def clean_slack_channel_leftovers(slack_team_identity_id, slack_channel_id): ) return - orgs_to_clean_general_log_channel_id = [] for org in sti.organizations.all(): - if org.general_log_channel_id == slack_channel_id: - logger.info( - f"Set general_log_channel_id to None for org_id={org.id} slack_channel_id={slack_channel_id} since slack_channel is arcived or deleted" - ) - org.general_log_channel_id = None - orgs_to_clean_general_log_channel_id.append(org) ChannelFilter.objects.filter(alert_receive_channel__organization=org, slack_channel_id=slack_channel_id).update( slack_channel_id=None ) - - Organization.objects.bulk_update(orgs_to_clean_general_log_channel_id, ["general_log_channel_id"], batch_size=5000) diff --git a/engine/apps/slack/tests/test_reset_slack.py b/engine/apps/slack/tests/test_reset_slack.py index d54d7f0faa..5b4b160ccb 100644 --- a/engine/apps/slack/tests/test_reset_slack.py +++ b/engine/apps/slack/tests/test_reset_slack.py @@ -69,10 +69,19 @@ def test_clean_slack_integration_leftovers( @pytest.mark.django_db def test_unpopulate_slack_user_identities( - make_organization_and_user_with_slack_identities, make_user_with_slack_user_identity + make_slack_team_identity, + make_slack_channel, + make_organization, + make_user_for_organization, + make_user_with_slack_user_identity, ): # create organization and user with Slack connected - organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities() + slack_team_identity = make_slack_team_identity() + slack_channel = make_slack_channel(slack_team_identity) + organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel) + user = make_user_for_organization(organization) + + assert organization.default_slack_channel_slack_id is not None # create & delete user with Slack connected deleted_user, _ = make_user_with_slack_user_identity(slack_team_identity, organization) @@ -90,4 +99,4 @@ def test_unpopulate_slack_user_identities( # check that Slack specific info is reset for organization assert organization.slack_team_identity is None - assert organization.general_log_channel_id is None + assert organization.default_slack_channel_slack_id is None diff --git a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py index 00d029319d..9da50f7bb8 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py @@ -55,6 +55,7 @@ def test_skip_escalations_error( @pytest.mark.django_db def test_timeout_error( make_slack_team_identity, + make_slack_channel, make_organization, make_alert_receive_channel, make_alert_group, @@ -62,9 +63,8 @@ def test_timeout_error( ): SlackAlertShootingStep = ScenarioStep.get_step("distribute_alerts", "AlertShootingStep") slack_team_identity = make_slack_team_identity() - organization = make_organization( - slack_team_identity=slack_team_identity, general_log_channel_id="DEFAULT_CHANNEL_ID" - ) + slack_channel = make_slack_channel(slack_team_identity) + organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) alert = make_alert(alert_group, raw_request_data="{}") @@ -89,15 +89,15 @@ def test_timeout_error( def test_alert_shooting_no_channel_filter( mock_post_alert_group_to_slack, make_slack_team_identity, + make_slack_channel, make_organization, make_alert_receive_channel, make_alert_group, make_alert, ): slack_team_identity = make_slack_team_identity() - organization = make_organization( - slack_team_identity=slack_team_identity, general_log_channel_id="DEFAULT_CHANNEL_ID" - ) + slack_channel = make_slack_channel(slack_team_identity, slack_id="DEFAULT_CHANNEL_ID") + organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel) alert_receive_channel = make_alert_receive_channel(organization) # simulate an alert group with channel filter deleted in the middle of the escalation diff --git a/engine/apps/user_management/migrations/0025_organization_default_slack_channel.py b/engine/apps/user_management/migrations/0025_organization_default_slack_channel.py new file mode 100644 index 0000000000..6e4c405451 --- /dev/null +++ b/engine/apps/user_management/migrations/0025_organization_default_slack_channel.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.16 on 2024-10-17 19:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('slack', '0005_slackteamidentity__unified_slack_app_installed'), + ('user_management', '0024_organization_direct_paging_prefer_important_policy'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='default_slack_channel', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='slack.slackchannel'), + ), + ] diff --git a/engine/apps/user_management/migrations/0026_auto_20241017_1919.py b/engine/apps/user_management/migrations/0026_auto_20241017_1919.py new file mode 100644 index 0000000000..c086921728 --- /dev/null +++ b/engine/apps/user_management/migrations/0026_auto_20241017_1919.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.15 on 2024-10-17 19:19 +import logging + +from django.db import migrations +import django_migration_linter as linter + +logger = logging.getLogger(__name__) + + +def populate_default_slack_channel(apps, schema_editor): + Organization = apps.get_model("user_management", "Organization") + SlackChannel = apps.get_model("slack", "SlackChannel") + + logger.info("Starting migration to populate default_slack_channel field.") + + queryset = Organization.objects.filter(general_log_channel_id__isnull=False, slack_team_identity__isnull=False) + total_orgs = queryset.count() + updated_orgs = 0 + missing_channels = 0 + organizations_to_update = [] + + logger.info(f"Total organizations to process: {total_orgs}") + + for org in queryset: + slack_id = org.general_log_channel_id + slack_team_identity = org.slack_team_identity + + try: + slack_channel = SlackChannel.objects.get(slack_id=slack_id, slack_team_identity=slack_team_identity) + + org.default_slack_channel = slack_channel + organizations_to_update.append(org) + + updated_orgs += 1 + logger.info( + f"Organization {org.id} updated with SlackChannel {slack_channel.id} (slack_id: {slack_id})." + ) + except SlackChannel.DoesNotExist: + missing_channels += 1 + logger.warning( + f"SlackChannel with slack_id {slack_id} and slack_team_identity {slack_team_identity} " + f"does not exist for Organization {org.id}." + ) + + if organizations_to_update: + Organization.objects.bulk_update(organizations_to_update, ["default_slack_channel"]) + logger.info(f"Bulk updated {len(organizations_to_update)} organizations with their default Slack channel.") + + logger.info( + f"Finished migration. Total organizations processed: {total_orgs}. " + f"Organizations updated: {updated_orgs}. Missing SlackChannels: {missing_channels}." + ) + +class Migration(migrations.Migration): + + dependencies = [ + ("user_management", "0025_organization_default_slack_channel"), + ] + + operations = [ + # simply setting this new field is okay, we are not deleting the value of general_log_channel_id + # therefore, no need to revert it + linter.IgnoreMigration(), + migrations.RunPython(populate_default_slack_channel, migrations.RunPython.noop), + ] diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index d9e74a432b..aac0aeae9a 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -34,7 +34,7 @@ ) from apps.mobile_app.models import MobileAppAuthToken from apps.schedules.models import CustomOnCallShift, OnCallSchedule - from apps.slack.models import SlackTeamIdentity + from apps.slack.models import SlackChannel, SlackTeamIdentity from apps.telegram.models import TelegramToOrganizationConnector from apps.user_management.models import Region, Team, User @@ -89,6 +89,7 @@ class Organization(MaintainableObject): alert_receive_channels: "RelatedManager['AlertReceiveChannel']" auth_tokens: "RelatedManager['ApiAuthToken']" custom_on_call_shifts: "RelatedManager['CustomOnCallShift']" + default_slack_channel: typing.Optional["SlackChannel"] migration_destination: typing.Optional["Region"] mobile_app_auth_tokens: "RelatedManager['MobileAppAuthToken']" oncall_schedules: "RelatedManager['OnCallSchedule']" @@ -103,25 +104,6 @@ class Organization(MaintainableObject): objects: models.Manager["Organization"] = OrganizationManager() objects_with_deleted = models.Manager() - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.subscription_strategy = self._get_subscription_strategy() - - def delete(self): - if settings.FEATURE_MULTIREGION_ENABLED: - unregister_oncall_tenant(str(self.uuid), settings.ONCALL_BACKEND_REGION) - if self.slack_team_identity and not settings.UNIFIED_SLACK_APP_ENABLED: - unlink_slack_team(str(self.uuid), self.slack_team_identity.slack_id) - self.deleted_at = timezone.now() - self.save(update_fields=["deleted_at"]) - - def hard_delete(self): - super().delete() - - def _get_subscription_strategy(self): - if self.pricing_version == self.FREE_PUBLIC_BETA_PRICING: - return FreePublicBetaSubscriptionStrategy(self) - public_primary_key = models.CharField( max_length=20, validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)], @@ -181,8 +163,15 @@ def _get_subscription_strategy(self): "slack.SlackTeamIdentity", on_delete=models.PROTECT, null=True, default=None, related_name="organizations" ) - # Slack specific field with general log channel id + # TODO: drop this field in a subsequent release, this has been migrated to default_slack_channel field general_log_channel_id = models.CharField(max_length=100, null=True, default=None) + default_slack_channel = models.ForeignKey( + "slack.SlackChannel", + null=True, + default=None, + on_delete=models.SET_NULL, + related_name="+", + ) # uuid used to unuqie identify organization in different clusters uuid = models.UUIDField(default=uuid.uuid4, editable=False) @@ -264,6 +253,25 @@ def _get_subscription_strategy(self): class Meta: unique_together = ("stack_id", "org_id") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.subscription_strategy = self._get_subscription_strategy() + + def delete(self): + if settings.FEATURE_MULTIREGION_ENABLED: + unregister_oncall_tenant(str(self.uuid), settings.ONCALL_BACKEND_REGION) + if self.slack_team_identity and not settings.UNIFIED_SLACK_APP_ENABLED: + unlink_slack_team(str(self.uuid), self.slack_team_identity.slack_id) + self.deleted_at = timezone.now() + self.save(update_fields=["deleted_at"]) + + def hard_delete(self): + super().delete() + + def _get_subscription_strategy(self): + if self.pricing_version == self.FREE_PUBLIC_BETA_PRICING: + return FreePublicBetaSubscriptionStrategy(self) + def provision_plugin(self) -> ProvisionedPlugin: from apps.auth_token.models import PluginAuthToken @@ -301,20 +309,20 @@ def update_alert_group_table_columns(self, columns: typing.List[AlertGroupTableC self.alert_group_table_columns = columns self.save(update_fields=["alert_group_table_columns"]) - def set_general_log_channel(self, channel_id, channel_name, user): - if self.general_log_channel_id != channel_id: - old_general_log_channel_id = self.slack_team_identity.cached_channels.filter( - slack_id=self.general_log_channel_id - ).first() - old_channel_name = old_general_log_channel_id.name if old_general_log_channel_id else None - self.general_log_channel_id = channel_id - self.save(update_fields=["general_log_channel_id"]) + def set_default_slack_channel(self, slack_channel: "SlackChannel", user: "User") -> None: + if self.default_slack_channel != slack_channel: + old_default_slack_channel = self.default_slack_channel + old_channel_name = old_default_slack_channel.name if old_default_slack_channel else None + + self.default_slack_channel = slack_channel + self.save(update_fields=["default_slack_channel"]) + write_chatops_insight_log( author=user, event_name=ChatOpsEvent.DEFAULT_CHANNEL_CHANGED, chatops_type=ChatOpsTypePlug.SLACK.value, prev_channel=old_channel_name, - new_channel=channel_name, + new_channel=slack_channel.name, ) def get_notifiable_direct_paging_integrations(self) -> "RelatedManager['AlertReceiveChannel']": @@ -348,6 +356,10 @@ def get_notifiable_direct_paging_integrations(self) -> "RelatedManager['AlertRec .distinct() ) + @property + def default_slack_channel_slack_id(self) -> typing.Optional[str]: + return self.default_slack_channel.slack_id if self.default_slack_channel else None + @property def web_link_with_uuid(self): """ diff --git a/engine/common/insight_log/chatops_insight_logs.py b/engine/common/insight_log/chatops_insight_logs.py index 64808beb10..f6de200b66 100644 --- a/engine/common/insight_log/chatops_insight_logs.py +++ b/engine/common/insight_log/chatops_insight_logs.py @@ -1,9 +1,13 @@ import enum import json import logging +import typing from .insight_logs_enabled_check import is_insight_logs_enabled +if typing.TYPE_CHECKING: + from apps.user_management.models import User + insight_logger = logging.getLogger("insight_logger") logger = logging.getLogger(__name__) @@ -24,7 +28,7 @@ class ChatOpsTypePlug(enum.Enum): TELEGRAM = "telegram" -def write_chatops_insight_log(author, event_name: ChatOpsEvent, chatops_type: str, **kwargs): +def write_chatops_insight_log(author: "User", event_name: ChatOpsEvent, chatops_type: str, **kwargs): try: organization = author.organization diff --git a/engine/common/insight_log/insight_logs_enabled_check.py b/engine/common/insight_log/insight_logs_enabled_check.py index 67041bd9fc..be24250b57 100644 --- a/engine/common/insight_log/insight_logs_enabled_check.py +++ b/engine/common/insight_log/insight_logs_enabled_check.py @@ -1,11 +1,15 @@ import logging +import typing from django.conf import settings +if typing.TYPE_CHECKING: + from apps.user_management.models import Organization + logger = logging.getLogger(__name__) -def is_insight_logs_enabled(organization): +def is_insight_logs_enabled(organization: "Organization") -> bool: """ is_insight_logs_enabled checks if inside logs enabled for given organization. Now it checks if oncall is deployed on same cluster that its grafana instance to be able to forward logs