oncall-engine/engine/common/incident_api/client.py
Matias Bordese 4d9846eeb4
Clean up reverted migration (#5119)
Related to https://github.com/grafana/oncall/pull/5116

Simplifies the db migration removing the `DeclaredIncident` model + FK
setup but keeping the other changes (adding `severity` field for
escalation policy, and "Declare incident" step, which is disabled). In
this way deployments for which the original migration was run, this
won't be applied and they will be in sync with the migration status
(eventually a manual step may be needed to remove the table and FK,
which won't be used for now).
2024-10-03 16:51:40 +00:00

165 lines
5.2 KiB
Python

import typing
from json import JSONDecodeError
from urllib.parse import urljoin
import requests
from django.conf import settings
from common.constants.plugin_ids import PluginID
class IncidentDetails(typing.TypedDict):
# https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/api/reference/#getincidentresponse
createdByUser: typing.Dict
createdTime: str
durationSeconds: int
heroImagePath: str
incidentEnd: str
incidentID: str
incidentMembership: typing.Dict
incidentStart: str
isDrill: bool
labels: typing.List[dict]
overviewURL: str
severity: str
status: str
summary: str
taskList: typing.Dict
title: str
class SeverityDetails(typing.TypedDict):
severityID: str
orgID: str
displayLabel: str
level: int
iconName: str
description: str
darkColor: str
lightColor: str
archivedAt: str
archivedByUserID: str | None
deletedAt: str
deletedByUserID: str | None
createdAt: str
updatedAt: str
class ActivityItemDetails(typing.TypedDict):
# https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/api/reference/#addactivityresponse
activityItemID: str
activityKind: str
attachments: typing.List[dict]
body: str
createdTime: str
eventTime: str
fieldValues: typing.Dict[str, str]
immutable: bool
incidentID: str
relevance: str
subjectUser: typing.Dict[str, str]
tags: typing.List[str]
url: str
user: typing.Dict[str, str]
class IncidentAPIException(Exception):
def __init__(self, status, url, msg="", method="GET"):
self.url = url
self.status = status
self.method = method
self.msg = msg
def __str__(self):
return f"IncidentAPIException: status={self.status} url={self.url} method={self.method}"
TIMEOUT = 5
DEFAULT_INCIDENT_SEVERITY = "Pending"
DEFAULT_INCIDENT_STATUS = "active"
DEFAULT_ACTIVITY_KIND = "userNote"
class IncidentAPIClient:
INCIDENT_BASE_PATH = f"/api/plugins/{PluginID.INCIDENT}/resources/"
def __init__(self, api_url: str, api_token: str) -> None:
self.api_token = api_token
self.api_url = urljoin(api_url, self.INCIDENT_BASE_PATH)
@property
def _request_headers(self):
return {"User-Agent": settings.GRAFANA_COM_USER_AGENT, "Authorization": f"Bearer {self.api_token}"}
def _check_response(self, response: requests.models.Response):
message = ""
if response.status_code >= 400:
try:
error_data = response.json()
message = error_data.get("error", response.reason)
except JSONDecodeError:
message = response.reason
raise IncidentAPIException(
status=response.status_code,
url=response.request.url,
msg=message,
method=response.request.method,
)
def create_incident(
self,
title: str,
severity: str = DEFAULT_INCIDENT_SEVERITY,
status: str = DEFAULT_INCIDENT_STATUS,
attachCaption: str = "",
attachURL: str = "",
) -> typing.Tuple[IncidentDetails, requests.models.Response]:
endpoint = "api/v1/IncidentsService.CreateIncident"
url = self.api_url + endpoint
# NOTE: invalid severity will raise a 500 error
response = requests.post(
url,
json={
"title": title,
"severity": severity,
"attachCaption": attachCaption,
"attachURL": attachURL,
"status": status,
},
timeout=TIMEOUT,
headers=self._request_headers,
)
self._check_response(response)
return response.json().get("incident"), response
def get_incident(self, incident_id: str) -> typing.Tuple[IncidentDetails, requests.models.Response]:
endpoint = "api/v1/IncidentsService.GetIncident"
url = self.api_url + endpoint
response = requests.post(url, json={"incidentID": incident_id}, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json().get("incident"), response
def get_severities(self) -> typing.Tuple[typing.List[SeverityDetails], requests.models.Response]:
# NOTE: internal endpoint
endpoint = "api/SeveritiesService.GetOrgSeverities"
url = self.api_url + endpoint
# pass empty json payload otherwise it will return a 500 response
response = requests.post(url, timeout=TIMEOUT, headers=self._request_headers, json={})
self._check_response(response)
return response.json().get("severities"), response
def add_activity(
self, incident_id: str, body: str, kind: str = DEFAULT_ACTIVITY_KIND
) -> typing.Tuple[ActivityItemDetails, requests.models.Response]:
endpoint = "api/v1/ActivityService.AddActivity"
url = self.api_url + endpoint
response = requests.post(
url,
json={"incidentID": incident_id, "activityKind": kind, "body": body},
timeout=TIMEOUT,
headers=self._request_headers,
)
self._check_response(response)
return response.json().get("activityItem"), response