add POST /escalation public API endpoint + add public API docs for teams/organization endpoints (#4815)
# What this PR does - Adds a `POST /escalation` public endpoint (equivalent to the internal direct paging API endpoint) - Adds public API documentation for teams and organization endpoints <img width="1140" alt="Screenshot 2024-08-15 at 12 49 40" src="https://github.com/user-attachments/assets/e0e8d6bb-f3ac-4f9e-bdf7-e8926949cc3b"> ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall-private/issues/2859 Closes https://github.com/grafana/oncall/issues/2448 ## 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] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
This commit is contained in:
parent
64bf1e5096
commit
67fc52d56a
13 changed files with 752 additions and 6 deletions
190
docs/sources/oncall-api-reference/escalation.md
Normal file
190
docs/sources/oncall-api-reference/escalation.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
---
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation/
|
||||
title: Escalation HTTP API
|
||||
weight: 1200
|
||||
refs:
|
||||
users:
|
||||
- pattern: /docs/oncall/
|
||||
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/users
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/users
|
||||
teams:
|
||||
- pattern: /docs/oncall/
|
||||
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/teams
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/teams
|
||||
manual-paging:
|
||||
- pattern: /docs/oncall/
|
||||
destination: /docs/oncall/<ONCALL_VERSION>/configure/integrations/references/manual
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/configure/integrations/references/manual
|
||||
---
|
||||
|
||||
# Escalation HTTP API
|
||||
|
||||
See [Manual paging integration](ref:manual-paging) for more background on how escalating to a team or user(s) works.
|
||||
|
||||
## Escalate to a set of users
|
||||
|
||||
For more details about how to fetch a user's Grafana OnCall ID, refer to the [Users](ref:users) public API documentation.
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/escalation/" \
|
||||
--request POST \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"title": "We are seeing a network outage in the datacenter",
|
||||
"message": "I need help investigating, can you join the investigation?",
|
||||
"source_url": "https://github.com/myorg/myrepo/issues/123",
|
||||
"users": [
|
||||
{
|
||||
"id": "U281SN24AVVJX",
|
||||
"important": false
|
||||
},
|
||||
{
|
||||
"id": "U5AKCVNDEDUE7",
|
||||
"important": true
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "IZHCC4GTNPZ93",
|
||||
"integration_id": "CC3GZYZNIIEH5",
|
||||
"route_id": "RDN8LITALJXCJ",
|
||||
"alerts_count": 1,
|
||||
"state": "firing",
|
||||
"created_at": "2024-08-15T18:05:36.801215Z",
|
||||
"resolved_at": null,
|
||||
"resolved_by": null,
|
||||
"acknowledged_at": null,
|
||||
"acknowledged_by": null,
|
||||
"title": "We're seeing a network outage in the datacenter",
|
||||
"permalinks": {
|
||||
"slack": null,
|
||||
"slack_app": null,
|
||||
"telegram": null,
|
||||
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
|
||||
},
|
||||
"silenced_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## Escalate to a team
|
||||
|
||||
For more details about how to fetch a team's Grafana OnCall ID, refer to the [Teams](ref:teams) public API documentation.
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/escalation/" \
|
||||
--request POST \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"title": "We are seeing a network outage in the datacenter",
|
||||
"message": "I need help investigating, can you join the investigation?",
|
||||
"source_url": "https://github.com/myorg/myrepo/issues/123",
|
||||
"team": "TI73TDU19W48J"
|
||||
}'
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "IZHCC4GTNPZ93",
|
||||
"integration_id": "CC3GZYZNIIEH5",
|
||||
"route_id": "RDN8LITALJXCJ",
|
||||
"alerts_count": 1,
|
||||
"state": "firing",
|
||||
"created_at": "2024-08-15T18:05:36.801215Z",
|
||||
"resolved_at": null,
|
||||
"resolved_by": null,
|
||||
"acknowledged_at": null,
|
||||
"acknowledged_by": null,
|
||||
"title": "We're seeing a network outage in the datacenter",
|
||||
"permalinks": {
|
||||
"slack": null,
|
||||
"slack_app": null,
|
||||
"telegram": null,
|
||||
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
|
||||
},
|
||||
"silenced_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## Escalate to a set of user(s) for an existing Alert Group
|
||||
|
||||
The following shows how you can escalate to a set of user(s) for an existing Alert Group.
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/escalation/" \
|
||||
--request POST \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"alert_group_id": "IZMRNNY8RFS94",
|
||||
"users": [
|
||||
{
|
||||
"id": "U281SN24AVVJX",
|
||||
"important": false
|
||||
},
|
||||
{
|
||||
"id": "U5AKCVNDEDUE7",
|
||||
"important": true
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "IZHCC4GTNPZ93",
|
||||
"integration_id": "CC3GZYZNIIEH5",
|
||||
"route_id": "RDN8LITALJXCJ",
|
||||
"alerts_count": 1,
|
||||
"state": "firing",
|
||||
"created_at": "2024-08-15T18:05:36.801215Z",
|
||||
"resolved_at": null,
|
||||
"resolved_by": null,
|
||||
"acknowledged_at": null,
|
||||
"acknowledged_by": null,
|
||||
"title": "We're seeing a network outage in the datacenter",
|
||||
"permalinks": {
|
||||
"slack": null,
|
||||
"slack_app": null,
|
||||
"telegram": null,
|
||||
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
|
||||
},
|
||||
"silenced_at": null
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Unique | Required | Description |
|
||||
| -------------------- | :----: | :--------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `title` | No | No | Name of the Alert Group that will be created |
|
||||
| `message` | No | No | Content of the Alert Group that will be created |
|
||||
| `source_url` | No | No | Value that will be added in the Alert's payload as `oncall.permalink`. This can be useful to have the source URL/button autopopulated with a URL of interest. |
|
||||
| `team` | No | Yes (see [Things to Note](#things-to-note)) | Grafana OnCall team ID. If specified, will use the "Direct Paging" Integration associated with this Grafana OnCall team, to create the Alert Group. |
|
||||
| `users` | No | Yes (see [Things to Note](#things-to-note)) | List of user(s) to escalate to. See above request example for object schema. `id` represents the Grafana OnCall user's ID. `important` is a boolean representing whether to escalate the Alert Group using this user's default or important personal notification policy. |
|
||||
| `alert_group_id` | No | No | If specified, will escalate the specified users for this Alert Group. |
|
||||
|
||||
## Things to note
|
||||
|
||||
- `team` and `users` are mutually exclusive in the request payload. If you would like to escalate to a team AND user(s),
|
||||
first escalate to a team, then using the Alert Group ID returned in the response payload, add the required users to the
|
||||
existing Alert Group
|
||||
- `alert_group_id` is mutually exclusive with `title`, `message`, and `source_url`. Practically speaking this means that
|
||||
if you are trying to escalate to a set of users on an existing Alert Group, you cannot update the `title`, `message`, or
|
||||
`source_url` of that Alert Group
|
||||
- If escalating to a set of users for an existing Alert Group, the Alert Group cannot be in a resolved state
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`POST {{API_URL}}/api/v1/escalation/`
|
||||
73
docs/sources/oncall-api-reference/organizations.md
Normal file
73
docs/sources/oncall-api-reference/organizations.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/organizations/
|
||||
title: Grafana OnCall organizations HTTP API
|
||||
weight: 1500
|
||||
refs:
|
||||
pagination:
|
||||
- pattern: /docs/oncall/
|
||||
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/#pagination
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/#pagination
|
||||
---
|
||||
|
||||
# Grafana OnCall organizations HTTP API
|
||||
|
||||
## Get an organization
|
||||
|
||||
This endpoint retrieves the organization object.
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/organizations/O53AAGWFBPE5W/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
````
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "O53AAGWFBPE5W"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`GET {{API_URL}}/api/v1/organizations/<ORGANIZATION_ID>/`
|
||||
|
||||
| Parameter | Unique | Description |
|
||||
| ---------- | :-----: | :----------------------------------------------------------------- |
|
||||
| `id` | Yes | Organization ID |
|
||||
|
||||
## List Organizations
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/organizations/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "O53AAGWFBPE5W"
|
||||
}
|
||||
],
|
||||
"page_size": 25,
|
||||
"current_page_number": 1,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The response is [paginated](ref:pagination). You may need to make multiple requests to get all records.
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`GET {{API_URL}}/api/v1/organizations/`
|
||||
86
docs/sources/oncall-api-reference/teams.md
Normal file
86
docs/sources/oncall-api-reference/teams.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/teams/
|
||||
title: Grafana OnCall teams HTTP API
|
||||
weight: 1500
|
||||
refs:
|
||||
pagination:
|
||||
- pattern: /docs/oncall/
|
||||
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/#pagination
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/#pagination
|
||||
---
|
||||
|
||||
# Grafana OnCall teams HTTP API
|
||||
|
||||
## Get a team
|
||||
|
||||
This endpoint retrieves the team object.
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/teams/TI73TDU19W48J/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
````
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "TI73TDU19W48J",
|
||||
"name": "my test team",
|
||||
"email": "",
|
||||
"avatar_url": "/avatar/3f49c15916554246daa714b9bd0ee398"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`GET {{API_URL}}/api/v1/teams/<TEAM_ID>/`
|
||||
|
||||
| Parameter | Unique | Description |
|
||||
| ---------- | :-----: | :----------------------------------------------------------------- |
|
||||
| `id` | Yes/org | Team ID |
|
||||
| `name` | Yes/org | Team name |
|
||||
| `email` | Yes/org | Team e-mail |
|
||||
| `avatar_url` | Yes | Avatar URL of the Grafana team |
|
||||
|
||||
## List Teams
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/teams/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "TI73TDU19W48J",
|
||||
"name": "my test team",
|
||||
"email": "",
|
||||
"avatar_url": "/avatar/3f49c15916554246daa714b9bd0ee398"
|
||||
}
|
||||
],
|
||||
"page_size": 50,
|
||||
"current_page_number": 1,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The response is [paginated](ref:pagination). You may need to make multiple requests to get all records.
|
||||
|
||||
The following available filter parameter should be provided as a `GET` argument:
|
||||
|
||||
- `name` (Exact match)
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`GET {{API_URL}}/api/v1/teams/`
|
||||
|
|
@ -32,9 +32,11 @@ class UserReferenceSerializer(serializers.Serializer):
|
|||
return attrs
|
||||
|
||||
|
||||
class DirectPagingSerializer(serializers.Serializer):
|
||||
class BasePagingSerializer(serializers.Serializer):
|
||||
context: SerializerContext
|
||||
|
||||
ALLOWS_GRAFANA_INCIDENT_ID = False
|
||||
|
||||
users = UserReferenceSerializer(many=True, required=False, default=list)
|
||||
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())
|
||||
|
||||
|
|
@ -44,7 +46,6 @@ class DirectPagingSerializer(serializers.Serializer):
|
|||
title = serializers.CharField(required=False, default=None)
|
||||
message = serializers.CharField(required=False, default=None, allow_null=True)
|
||||
source_url = serializers.URLField(required=False, default=None, allow_null=True)
|
||||
grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
organization = self.context["organization"]
|
||||
|
|
@ -52,13 +53,17 @@ class DirectPagingSerializer(serializers.Serializer):
|
|||
title = attrs["title"]
|
||||
message = attrs["message"]
|
||||
source_url = attrs["source_url"]
|
||||
grafana_incident_id = attrs["grafana_incident_id"]
|
||||
grafana_incident_id = self.ALLOWS_GRAFANA_INCIDENT_ID and attrs.get("grafana_incident_id")
|
||||
|
||||
if alert_group_id and (title or message or source_url or grafana_incident_id):
|
||||
raise serializers.ValidationError(
|
||||
"alert_group_id and (title, message, source_url, grafana_incident_id) are mutually exclusive"
|
||||
f"alert_group_id and (title, message, source_url{', grafana_incident_id' if self.ALLOWS_GRAFANA_INCIDENT_ID else ''}) "
|
||||
"are mutually exclusive"
|
||||
)
|
||||
|
||||
if attrs["users"] and attrs["team"]:
|
||||
raise serializers.ValidationError("users and team are mutually exclusive")
|
||||
|
||||
if alert_group_id:
|
||||
try:
|
||||
attrs["alert_group"] = AlertGroup.objects.get(
|
||||
|
|
@ -68,3 +73,9 @@ class DirectPagingSerializer(serializers.Serializer):
|
|||
raise serializers.ValidationError("Alert group {} does not exist".format(alert_group_id))
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class DirectPagingSerializer(BasePagingSerializer):
|
||||
ALLOWS_GRAFANA_INCIDENT_ID = True
|
||||
|
||||
grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)
|
||||
|
|
@ -224,6 +224,41 @@ def test_direct_paging_no_user_or_team_specified(
|
|||
assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_direct_paging_both_team_and_users_specified(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_user_auth_headers,
|
||||
make_user,
|
||||
make_team,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR)
|
||||
team = make_team(organization=organization)
|
||||
|
||||
# user must be part of the team
|
||||
user.teams.add(team)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:direct_paging")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"team": team.public_primary_key,
|
||||
"users": [
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": False,
|
||||
},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["non_field_errors"] == ["users and team are mutually exclusive"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field_name,field_value",
|
||||
[
|
||||
|
|
@ -9,6 +9,7 @@ from .views.alert_receive_channel import AlertReceiveChannelView
|
|||
from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView
|
||||
from .views.alerts import AlertDetailView
|
||||
from .views.channel_filter import ChannelFilterView
|
||||
from .views.direct_paging import DirectPagingAPIView
|
||||
from .views.escalation_chain import EscalationChainViewSet
|
||||
from .views.escalation_policy import EscalationPolicyView
|
||||
from .views.features import FeaturesAPIView
|
||||
|
|
@ -23,7 +24,6 @@ from .views.organization import (
|
|||
OrganizationConfigChecksView,
|
||||
SetGeneralChannel,
|
||||
)
|
||||
from .views.paging import DirectPagingAPIView
|
||||
from .views.preview_template_options import PreviewTemplateOptionsView
|
||||
from .views.public_api_tokens import PublicApiTokenView
|
||||
from .views.resolution_note import ResolutionNoteView
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from rest_framework.views import APIView
|
|||
|
||||
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.api.serializers.paging import DirectPagingSerializer
|
||||
from apps.api.serializers.direct_paging import DirectPagingSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from .alerts import AlertSerializer # noqa: F401
|
||||
from .escalation import EscalationSerializer # noqa: F401
|
||||
from .escalation_chains import EscalationChainSerializer # noqa: F401
|
||||
from .escalation_policies import EscalationPolicySerializer, EscalationPolicyUpdateSerializer # noqa: F401
|
||||
from .incidents import IncidentSerializer # noqa: F401
|
||||
|
|
|
|||
8
engine/apps/public_api/serializers/escalation.py
Normal file
8
engine/apps/public_api/serializers/escalation.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from apps.api.serializers.direct_paging import BasePagingSerializer
|
||||
|
||||
|
||||
class EscalationSerializer(BasePagingSerializer):
|
||||
"""
|
||||
Very similar to `apps.api.serializers.direct_paging.DirectPagingSerializer` except that
|
||||
there is no `grafana_incident_id` attribute
|
||||
"""
|
||||
294
engine/apps/public_api/tests/test_escalation.py
Normal file
294
engine/apps/public_api/tests/test_escalation.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError
|
||||
|
||||
title = "Custom title"
|
||||
message = "Testing escalation with new alert group"
|
||||
source_url = "https://www.example.com"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_new_alert_group(
|
||||
make_organization_and_user_with_token,
|
||||
make_user,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
|
||||
users_to_page = [
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": False,
|
||||
},
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": True,
|
||||
},
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"users": users_to_page,
|
||||
"title": title,
|
||||
"message": message,
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
alert_groups = AlertGroup.objects.all()
|
||||
assert alert_groups.count() == 1
|
||||
ag = alert_groups.get()
|
||||
|
||||
assert response.json() == {
|
||||
"id": ag.public_primary_key,
|
||||
"integration_id": ag.channel.public_primary_key,
|
||||
"route_id": ag.channel_filter.public_primary_key,
|
||||
"alerts_count": 1,
|
||||
"state": "firing",
|
||||
"created_at": mock.ANY,
|
||||
"resolved_at": None,
|
||||
"resolved_by": None,
|
||||
"acknowledged_at": None,
|
||||
"acknowledged_by": None,
|
||||
"title": title,
|
||||
"permalinks": {
|
||||
"slack": None,
|
||||
"slack_app": None,
|
||||
"telegram": None,
|
||||
"web": f"a/grafana-oncall-app/alert-groups/{ag.public_primary_key}",
|
||||
},
|
||||
"silenced_at": None,
|
||||
}
|
||||
|
||||
alert = ag.alerts.get()
|
||||
|
||||
assert ag.web_title_cache == title
|
||||
assert alert.title == title
|
||||
assert alert.message == message
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_team(
|
||||
make_organization_and_user_with_token,
|
||||
make_team,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
team = make_team(organization=organization)
|
||||
|
||||
# user must be part of the team
|
||||
user.teams.add(team)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"team": team.public_primary_key,
|
||||
"message": message,
|
||||
"source_url": source_url,
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
alert_group = AlertGroup.objects.get(public_primary_key=response.json()["id"])
|
||||
alert = alert_group.alerts.first()
|
||||
|
||||
assert alert.raw_request_data["oncall"]["permalink"] == source_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_existing_alert_group(
|
||||
make_organization_and_user_with_token,
|
||||
make_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
|
||||
users_to_page = [
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": False,
|
||||
},
|
||||
{
|
||||
"id": make_user(
|
||||
organization=organization,
|
||||
).public_primary_key,
|
||||
"important": True,
|
||||
},
|
||||
]
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={"users": users_to_page, "alert_group_id": alert_group.public_primary_key},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["id"] == alert_group.public_primary_key
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_existing_alert_group_resolved(
|
||||
make_organization_and_user_with_token,
|
||||
make_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True)
|
||||
|
||||
users_to_page = [
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": False,
|
||||
},
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"alert_group_id": alert_group.public_primary_key,
|
||||
"users": users_to_page,
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["detail"] == DirectPagingAlertGroupResolvedError.DETAIL
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_no_user_or_team_specified(
|
||||
make_organization_and_user_with_token,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
_, user, token = make_organization_and_user_with_token()
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"team": None,
|
||||
"users": [],
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_both_team_and_users_specified(
|
||||
make_organization_and_user_with_token,
|
||||
make_user_auth_headers,
|
||||
make_user,
|
||||
make_team,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
team = make_team(organization=organization)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"team": team.public_primary_key,
|
||||
"users": [
|
||||
{
|
||||
"id": make_user(organization=organization).public_primary_key,
|
||||
"important": False,
|
||||
},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["non_field_errors"] == ["users and team are mutually exclusive"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field_name,field_value",
|
||||
[
|
||||
("title", title),
|
||||
("message", message),
|
||||
("source_url", source_url),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_alert_group_id_and_other_fields_are_mutually_exclusive(
|
||||
make_organization_and_user_with_token,
|
||||
make_team,
|
||||
make_user_auth_headers,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
field_name,
|
||||
field_value,
|
||||
):
|
||||
error_msg = "alert_group_id and (title, message, source_url) are mutually exclusive"
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
team = make_team(organization=organization)
|
||||
|
||||
# user must be part of the team
|
||||
user.teams.add(team)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation")
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
data={
|
||||
"team": team.public_primary_key,
|
||||
"alert_group_id": alert_group.public_primary_key,
|
||||
field_name: field_value,
|
||||
},
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["non_field_errors"] == [error_msg]
|
||||
|
|
@ -34,4 +34,5 @@ urlpatterns = [
|
|||
optional_slash_path("info", views.InfoView.as_view(), name="info"),
|
||||
optional_slash_path("make_call", views.MakeCallView.as_view(), name="make_call"),
|
||||
optional_slash_path("send_sms", views.SendSMSView.as_view(), name="send_sms"),
|
||||
optional_slash_path("escalation", views.EscalationView.as_view(), name="escalation"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from .action import ActionView # noqa: F401
|
||||
from .alerts import AlertView # noqa: F401
|
||||
from .escalation import EscalationView # noqa: F401
|
||||
from .escalation_chains import EscalationChainView # noqa: F401
|
||||
from .escalation_policies import EscalationPolicyView # noqa: F401
|
||||
from .incidents import IncidentView # noqa: F401
|
||||
|
|
|
|||
46
engine/apps/public_api/views/escalation.py
Normal file
46
engine/apps/public_api/views/escalation.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
|
||||
from apps.auth_token.auth import ApiTokenAuthentication
|
||||
from apps.public_api.serializers import EscalationSerializer, IncidentSerializer
|
||||
from apps.public_api.throttlers import UserThrottle
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
|
||||
|
||||
class EscalationView(APIView):
|
||||
"""
|
||||
aka "Direct Paging"
|
||||
"""
|
||||
|
||||
authentication_classes = (ApiTokenAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
throttle_classes = [UserThrottle]
|
||||
|
||||
def post(self, request):
|
||||
user = request.user
|
||||
organization = user.organization
|
||||
|
||||
serializer = EscalationSerializer(data=request.data, context={"organization": organization, "request": request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
try:
|
||||
alert_group = direct_paging(
|
||||
organization=organization,
|
||||
from_user=user,
|
||||
message=validated_data["message"],
|
||||
title=validated_data["title"],
|
||||
source_url=validated_data["source_url"],
|
||||
team=validated_data["team"],
|
||||
users=[(user["instance"], user["important"]) for user in validated_data["users"]],
|
||||
alert_group=validated_data["alert_group"],
|
||||
)
|
||||
except DirectPagingAlertGroupResolvedError:
|
||||
raise BadRequest(detail=DirectPagingAlertGroupResolvedError.DETAIL)
|
||||
except DirectPagingUserTeamValidationError:
|
||||
raise BadRequest(detail=DirectPagingUserTeamValidationError.DETAIL)
|
||||
return Response(IncidentSerializer(alert_group).data, status=status.HTTP_200_OK)
|
||||
Loading…
Add table
Reference in a new issue