oncall-engine/engine/apps/alerts/models/maintainable_object.py

171 lines
7.1 KiB
Python
Raw Permalink Normal View History

import datetime
from uuid import uuid4
import humanize
import pytz
from django.db import models, transaction
from django.utils import timezone
from common.exceptions import MaintenanceCouldNotBeStartedError
from common.insight_log import MaintenanceEvent, write_maintenance_insight_log
class MaintainableObject(models.Model):
class Meta:
abstract = True
DURATION_ONE_HOUR = datetime.timedelta(hours=1)
DURATION_THREE_HOURS = datetime.timedelta(hours=3)
DURATION_SIX_HOURS = datetime.timedelta(hours=6)
DURATION_TWELVE_HOURS = datetime.timedelta(hours=12)
DURATION_TWENTY_FOUR_HOURS = datetime.timedelta(hours=24)
MAINTENANCE_DURATION_CHOICES = (
(DURATION_ONE_HOUR, "1 hour"),
(DURATION_THREE_HOURS, "3 hours"),
(DURATION_SIX_HOURS, "6 hours"),
(DURATION_TWELVE_HOURS, "12 hours"),
(DURATION_TWENTY_FOUR_HOURS, "24 hours"),
)
maintenance_duration = models.DurationField(default=None, null=True, choices=MAINTENANCE_DURATION_CHOICES)
(DEBUG_MAINTENANCE, MAINTENANCE) = range(2)
DEBUG_MAINTENANCE_KEY = "Debug"
MAINTENANCE_KEY = "Maintenance"
MAINTENANCE_MODE_CHOICES = ((DEBUG_MAINTENANCE, DEBUG_MAINTENANCE_KEY), (MAINTENANCE, MAINTENANCE_KEY))
MAINTENANCE_VERBAL = {
DEBUG_MAINTENANCE: "Debug (silence all escalations)",
MAINTENANCE: "Maintenance (collect everything in one incident)",
}
maintenance_mode = models.IntegerField(default=None, null=True, choices=MAINTENANCE_MODE_CHOICES)
maintenance_uuid = models.CharField(max_length=250, unique=True, null=True, default=None)
maintenance_started_at = models.DateTimeField(null=True, default=None)
maintenance_author = models.ForeignKey(
"user_management.user", on_delete=models.SET_NULL, null=True, related_name="%(class)s_maintenances_created"
)
def start_disable_maintenance_task(self, countdown):
raise NotImplementedError
def get_organization(self):
raise NotImplementedError
def get_team(self):
raise NotImplementedError
def get_verbal(self):
raise NotImplementedError
def force_disable_maintenance(self, user):
raise NotImplementedError
def notify_about_maintenance_action(self, text, send_to_general_log_channel=True):
raise NotImplementedError
def start_maintenance(self, mode, maintenance_duration, user):
`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.alerts.models import Alert, AlertGroup, AlertReceiveChannel
with transaction.atomic():
_self = self.__class__.objects.select_for_update().get(pk=self.pk)
if _self.maintenance_mode is not None:
raise MaintenanceCouldNotBeStartedError("Already on maintenance")
organization = _self.get_organization()
team = _self.get_team()
verbal = _self.get_verbal()
user_verbal = user.get_username_with_slack_verbal()
duration_verbal = humanize.naturaldelta(maintenance_duration)
# NOTE: there could be multiple maintenance integrations in case of a race condition
# (no constraints at the db level, it shouldn't be an issue functionality-wise)
maintenance_integration = AlertReceiveChannel.objects_with_maintenance.filter(
organization=organization,
team=team,
integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE,
).last()
if maintenance_integration is None:
maintenance_integration = AlertReceiveChannel.create(
organization=organization,
team=team,
integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE,
author=user,
)
maintenance_uuid = _self.start_disable_maintenance_task(maintenance_duration)
_self.maintenance_duration = datetime.timedelta(seconds=maintenance_duration)
_self.maintenance_uuid = maintenance_uuid
_self.maintenance_mode = mode
_self.maintenance_started_at = timezone.now()
_self.maintenance_author = user
_self.save(
update_fields=[
"maintenance_duration",
"maintenance_uuid",
"maintenance_mode",
"maintenance_started_at",
"maintenance_author",
]
)
self.maintenance_duration = _self.maintenance_duration
self.maintenance_uuid = _self.maintenance_uuid
self.maintenance_mode = _self.maintenance_mode
self.maintenance_started_at = _self.maintenance_started_at
self.maintenance_author = _self.maintenance_author
if mode == AlertReceiveChannel.MAINTENANCE:
group = AlertGroup.objects.create(
distinction=uuid4(),
web_title_cache=f"Maintenance of {verbal} for {maintenance_duration}",
maintenance_uuid=maintenance_uuid,
channel_filter_id=maintenance_integration.default_channel_filter.pk,
channel=maintenance_integration,
)
title = f"Maintenance of {verbal} for {duration_verbal}"
message = (
f"Initiated by {user_verbal}."
f" During this time all alerts from integration will be collected here without escalations"
)
alert = Alert(
is_the_first_alert_in_group=True,
is_resolve_signal=False,
title=title,
message=message,
group=group,
raw_request_data={
"title": title,
"message": message,
},
)
alert.save()
write_maintenance_insight_log(self, user, MaintenanceEvent.STARTED)
if mode == AlertReceiveChannel.MAINTENANCE:
self.notify_about_maintenance_action(
f"Maintenance of {verbal}. Initiated by {user_verbal} for {duration_verbal}.",
send_to_general_log_channel=False,
)
else:
self.notify_about_maintenance_action(
f"Debug of {verbal}. Initiated by {user_verbal} for {duration_verbal}."
)
@property
def till_maintenance_timestamp(self) -> int | None:
if self.maintenance_started_at is not None and self.maintenance_duration is not None:
return int((self.maintenance_started_at + self.maintenance_duration).astimezone(pytz.UTC).timestamp())
return None
@property
def started_at_timestamp(self):
if self.maintenance_started_at is not None and self.maintenance_duration is not None:
return int(self.maintenance_started_at.astimezone(pytz.UTC).timestamp())
return None
@classmethod
def maintenance_duration_options_in_seconds(cls):
options_in_seconds = []
for ch in cls.MAINTENANCE_DURATION_CHOICES:
options_in_seconds.append(int(ch[0].total_seconds()))
return options_in_seconds