Updates
This commit is contained in:
733
ETB-API/sla_oncall/Documentations/SLA_ONCALL_API.md
Normal file
733
ETB-API/sla_oncall/Documentations/SLA_ONCALL_API.md
Normal file
@@ -0,0 +1,733 @@
|
||||
# SLA & On-Call Management API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The SLA & On-Call Management module provides comprehensive Service Level Agreement (SLA) tracking, escalation policies, and on-call rotation management for enterprise incident management systems.
|
||||
|
||||
## Features
|
||||
|
||||
### Dynamic SLAs
|
||||
- **Incident Type-Based SLAs**: Different SLA targets based on incident category, severity, and priority
|
||||
- **Business Hours Support**: SLA calculations that respect business hours and timezones
|
||||
- **Multiple SLA Types**: Response time, resolution time, acknowledgment time, and first response time
|
||||
- **Automatic SLA Instance Creation**: SLAs are automatically created when incidents are reported
|
||||
|
||||
### Escalation Policies
|
||||
- **Multi-Level Escalation**: Configurable escalation steps with different actions and timing
|
||||
- **Condition-Based Triggering**: Escalations triggered by SLA breaches, thresholds, or custom conditions
|
||||
- **Multi-Channel Notifications**: Email, SMS, Slack, Teams, and webhook notifications
|
||||
- **Integration with On-Call**: Automatic escalation to current on-call personnel
|
||||
|
||||
### On-Call Rotation Management
|
||||
- **Flexible Scheduling**: Weekly, daily, monthly, and custom rotation schedules
|
||||
- **External System Integration**: Built-in support for PagerDuty and OpsGenie
|
||||
- **Handoff Management**: Structured handoff processes with notes and tracking
|
||||
- **Performance Metrics**: Track incident handling and response times
|
||||
|
||||
### Business Hours Management
|
||||
- **Timezone Support**: Multiple timezone configurations
|
||||
- **Holiday Calendar**: Holiday and special day handling
|
||||
- **Day Overrides**: Custom hours for specific dates
|
||||
- **Weekend Configuration**: Separate weekend business hours
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Business Hours Management
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/business-hours/
|
||||
List all business hours configurations.
|
||||
|
||||
**Query Parameters:**
|
||||
- `is_active`: Filter by active status
|
||||
- `is_default`: Filter by default status
|
||||
- `timezone`: Filter by timezone
|
||||
- `search`: Search by name or description
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Standard Business Hours",
|
||||
"description": "Standard 9-5 business hours",
|
||||
"timezone": "UTC",
|
||||
"weekday_start": "09:00:00",
|
||||
"weekday_end": "17:00:00",
|
||||
"weekend_start": "10:00:00",
|
||||
"weekend_end": "16:00:00",
|
||||
"day_overrides": {},
|
||||
"holiday_calendar": [],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/business-hours/
|
||||
Create a new business hours configuration.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Custom Business Hours",
|
||||
"description": "Custom business hours for special team",
|
||||
"timezone": "America/New_York",
|
||||
"weekday_start": "08:00:00",
|
||||
"weekday_end": "18:00:00",
|
||||
"weekend_start": "10:00:00",
|
||||
"weekend_end": "16:00:00",
|
||||
"holiday_calendar": ["2024-12-25", "2024-01-01"],
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/business-hours/{id}/test_business_hours/
|
||||
Test if a given time is within business hours.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"test_time": "2024-01-08T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"is_business_hours": true,
|
||||
"test_time": "2024-01-08T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### SLA Definitions
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/sla-definitions/
|
||||
List all SLA definitions.
|
||||
|
||||
**Query Parameters:**
|
||||
- `sla_type`: Filter by SLA type (RESPONSE_TIME, RESOLUTION_TIME, etc.)
|
||||
- `is_active`: Filter by active status
|
||||
- `business_hours_only`: Filter by business hours requirement
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Critical Incident Response",
|
||||
"description": "SLA for critical incidents",
|
||||
"sla_type": "RESPONSE_TIME",
|
||||
"incident_categories": ["SYSTEM", "NETWORK"],
|
||||
"incident_severities": ["CRITICAL", "EMERGENCY"],
|
||||
"incident_priorities": ["P1"],
|
||||
"target_duration_minutes": 15,
|
||||
"business_hours_only": false,
|
||||
"business_hours": null,
|
||||
"business_hours_name": null,
|
||||
"escalation_enabled": true,
|
||||
"escalation_threshold_percent": 75.0,
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/sla-definitions/
|
||||
Create a new SLA definition.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "High Priority Response",
|
||||
"description": "SLA for high priority incidents",
|
||||
"sla_type": "RESPONSE_TIME",
|
||||
"incident_severities": ["HIGH"],
|
||||
"incident_priorities": ["P2"],
|
||||
"target_duration_minutes": 30,
|
||||
"business_hours_only": false,
|
||||
"escalation_enabled": true,
|
||||
"escalation_threshold_percent": 80.0,
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/sla-definitions/{id}/test_applicability/
|
||||
Test if SLA definition applies to a given incident.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"category": "SYSTEM",
|
||||
"severity": "HIGH",
|
||||
"priority": "P2"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"applies": true,
|
||||
"incident_data": {
|
||||
"category": "SYSTEM",
|
||||
"severity": "HIGH",
|
||||
"priority": "P2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### On-Call Rotations
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/oncall-rotations/
|
||||
List all on-call rotations.
|
||||
|
||||
**Query Parameters:**
|
||||
- `rotation_type`: Filter by rotation type (WEEKLY, DAILY, etc.)
|
||||
- `status`: Filter by status (ACTIVE, PAUSED, INACTIVE)
|
||||
- `external_system`: Filter by external system integration
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Primary On-Call Rotation",
|
||||
"description": "Primary rotation for incident response",
|
||||
"rotation_type": "WEEKLY",
|
||||
"status": "ACTIVE",
|
||||
"team_name": "Incident Response Team",
|
||||
"team_description": "Primary team responsible for incidents",
|
||||
"schedule_config": {
|
||||
"rotation_length_days": 7,
|
||||
"handoff_time": "09:00"
|
||||
},
|
||||
"timezone": "UTC",
|
||||
"external_system": "INTERNAL",
|
||||
"external_system_id": null,
|
||||
"integration_config": {},
|
||||
"current_oncall": {
|
||||
"user_id": "uuid",
|
||||
"username": "john.doe",
|
||||
"start_time": "2024-01-08T09:00:00Z",
|
||||
"end_time": "2024-01-15T09:00:00Z"
|
||||
},
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/oncall-rotations/{id}/current_oncall/
|
||||
Get the current on-call person for a rotation.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"rotation": "uuid",
|
||||
"rotation_name": "Primary On-Call Rotation",
|
||||
"user": "uuid",
|
||||
"user_name": "john.doe",
|
||||
"user_email": "john.doe@company.com",
|
||||
"start_time": "2024-01-08T09:00:00Z",
|
||||
"end_time": "2024-01-15T09:00:00Z",
|
||||
"status": "ACTIVE",
|
||||
"incidents_handled": 5,
|
||||
"response_time_avg": "00:15:30"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/oncall-rotations/{id}/upcoming_assignments/
|
||||
Get upcoming on-call assignments.
|
||||
|
||||
**Query Parameters:**
|
||||
- `days`: Number of days ahead to look (default: 30)
|
||||
|
||||
### SLA Instances
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/sla-instances/
|
||||
List all SLA instances.
|
||||
|
||||
**Query Parameters:**
|
||||
- `status`: Filter by status (ACTIVE, MET, BREACHED, CANCELLED)
|
||||
- `escalation_triggered`: Filter by escalation status
|
||||
- `sla_definition`: Filter by SLA definition
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 10,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"sla_definition": "uuid",
|
||||
"sla_definition_name": "Critical Incident Response",
|
||||
"incident": "uuid",
|
||||
"incident_title": "Database Connection Failure",
|
||||
"status": "ACTIVE",
|
||||
"target_time": "2024-01-08T15:15:00Z",
|
||||
"started_at": "2024-01-08T15:00:00Z",
|
||||
"met_at": null,
|
||||
"breached_at": null,
|
||||
"escalation_policy": "uuid",
|
||||
"escalation_triggered": false,
|
||||
"escalation_triggered_at": null,
|
||||
"escalation_level": 0,
|
||||
"response_time": null,
|
||||
"resolution_time": null,
|
||||
"is_breached": false,
|
||||
"time_remaining": "00:12:30",
|
||||
"breach_time": "00:00:00",
|
||||
"created_at": "2024-01-08T15:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/sla-instances/breached/
|
||||
Get all breached SLA instances.
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/sla-instances/at_risk/
|
||||
Get SLA instances at risk of breaching (within 15 minutes).
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/sla-instances/{id}/mark_met/
|
||||
Mark an SLA instance as met.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "SLA marked as met"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/sla-instances/{id}/mark_breached/
|
||||
Mark an SLA instance as breached.
|
||||
|
||||
### On-Call Assignments
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/oncall-assignments/
|
||||
List all on-call assignments.
|
||||
|
||||
**Query Parameters:**
|
||||
- `rotation`: Filter by rotation
|
||||
- `user`: Filter by user
|
||||
- `status`: Filter by status (SCHEDULED, ACTIVE, COMPLETED, CANCELLED)
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/oncall-assignments/
|
||||
Create a new on-call assignment.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"rotation": "uuid",
|
||||
"user": "uuid",
|
||||
"start_time": "2024-01-15T09:00:00Z",
|
||||
"end_time": "2024-01-22T09:00:00Z",
|
||||
"handoff_notes": "All systems stable, no pending incidents"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/oncall-assignments/{id}/handoff/
|
||||
Perform on-call handoff.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"handoff_notes": "Handing off to next person. 3 active incidents."
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/oncall-assignments/{id}/activate/
|
||||
Activate a scheduled assignment.
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/oncall-assignments/{id}/complete/
|
||||
Complete an active assignment.
|
||||
|
||||
### Escalation Policies
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/escalation-policies/
|
||||
List all escalation policies.
|
||||
|
||||
**Query Parameters:**
|
||||
- `escalation_type`: Filter by escalation type
|
||||
- `trigger_condition`: Filter by trigger condition
|
||||
- `is_active`: Filter by active status
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/escalation-policies/
|
||||
Create a new escalation policy.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Critical Escalation",
|
||||
"description": "Escalation for critical incidents",
|
||||
"escalation_type": "TIME_BASED",
|
||||
"trigger_condition": "SLA_THRESHOLD",
|
||||
"incident_severities": ["CRITICAL", "EMERGENCY"],
|
||||
"trigger_delay_minutes": 0,
|
||||
"escalation_steps": [
|
||||
{
|
||||
"level": 1,
|
||||
"delay_minutes": 5,
|
||||
"actions": ["notify_oncall", "notify_manager"],
|
||||
"channels": ["email", "sms"]
|
||||
},
|
||||
{
|
||||
"level": 2,
|
||||
"delay_minutes": 15,
|
||||
"actions": ["notify_director", "page_oncall"],
|
||||
"channels": ["email", "sms", "phone"]
|
||||
}
|
||||
],
|
||||
"notification_channels": ["email", "sms", "phone"],
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
### Escalation Instances
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/escalation-instances/
|
||||
List all escalation instances.
|
||||
|
||||
**Query Parameters:**
|
||||
- `status`: Filter by status (PENDING, TRIGGERED, ACKNOWLEDGED, RESOLVED, CANCELLED)
|
||||
- `escalation_level`: Filter by escalation level
|
||||
- `escalation_policy`: Filter by escalation policy
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/escalation-instances/{id}/acknowledge/
|
||||
Acknowledge an escalation.
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/escalation-instances/{id}/resolve/
|
||||
Resolve an escalation.
|
||||
|
||||
### Notification Templates
|
||||
|
||||
#### GET /api/sla-oncall/api/v1/notification-templates/
|
||||
List all notification templates.
|
||||
|
||||
**Query Parameters:**
|
||||
- `template_type`: Filter by template type (ESCALATION, ONCALL_HANDOFF, etc.)
|
||||
- `channel_type`: Filter by channel type (EMAIL, SMS, SLACK, etc.)
|
||||
- `is_active`: Filter by active status
|
||||
|
||||
#### POST /api/sla-oncall/api/v1/notification-templates/
|
||||
Create a new notification template.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Email Escalation Alert",
|
||||
"template_type": "ESCALATION",
|
||||
"channel_type": "EMAIL",
|
||||
"subject_template": "URGENT: Incident #{incident_id} Escalated",
|
||||
"body_template": "Incident #{incident_id} has been escalated to Level {escalation_level}. Please respond immediately.",
|
||||
"variables": ["incident_id", "incident_title", "escalation_level"],
|
||||
"is_active": true,
|
||||
"is_default": true
|
||||
}
|
||||
```
|
||||
|
||||
## Setup and Configuration
|
||||
|
||||
### Initial Setup
|
||||
|
||||
Run the setup command to create default configurations:
|
||||
|
||||
```bash
|
||||
python manage.py setup_sla_oncall
|
||||
```
|
||||
|
||||
This command creates:
|
||||
- Default business hours configurations
|
||||
- Standard SLA definitions for different incident types
|
||||
- Default escalation policies
|
||||
- Notification templates
|
||||
- Sample on-call rotation (if users exist)
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
#### Business Hours for Different Teams
|
||||
|
||||
```python
|
||||
# 24/7 Operations
|
||||
business_hours = BusinessHours.objects.create(
|
||||
name='24/7 Operations',
|
||||
description='Always business hours',
|
||||
timezone='UTC',
|
||||
weekday_start=time(0, 0),
|
||||
weekday_end=time(23, 59),
|
||||
weekend_start=time(0, 0),
|
||||
weekend_end=time(23, 59),
|
||||
)
|
||||
|
||||
# EMEA Business Hours
|
||||
business_hours = BusinessHours.objects.create(
|
||||
name='EMEA Business Hours',
|
||||
description='EMEA timezone business hours',
|
||||
timezone='Europe/London',
|
||||
weekday_start=time(9, 0),
|
||||
weekday_end=time(17, 0),
|
||||
weekend_start=time(10, 0),
|
||||
weekend_end=time(16, 0),
|
||||
holiday_calendar=['2024-12-25', '2024-01-01', '2024-04-19'],
|
||||
)
|
||||
```
|
||||
|
||||
#### SLA Definitions
|
||||
|
||||
```python
|
||||
# Critical incidents - 15 minute response
|
||||
critical_sla = SLADefinition.objects.create(
|
||||
name='Critical Incident Response',
|
||||
description='SLA for critical and emergency incidents',
|
||||
sla_type='RESPONSE_TIME',
|
||||
incident_severities=['CRITICAL', 'EMERGENCY'],
|
||||
incident_priorities=['P1'],
|
||||
target_duration_minutes=15,
|
||||
business_hours_only=False,
|
||||
escalation_enabled=True,
|
||||
escalation_threshold_percent=75.0,
|
||||
)
|
||||
|
||||
# Medium incidents - 2 hour response during business hours
|
||||
medium_sla = SLADefinition.objects.create(
|
||||
name='Medium Priority Response',
|
||||
description='SLA for medium priority incidents',
|
||||
sla_type='RESPONSE_TIME',
|
||||
incident_severities=['MEDIUM'],
|
||||
incident_priorities=['P3'],
|
||||
target_duration_minutes=120,
|
||||
business_hours_only=True,
|
||||
business_hours=business_hours,
|
||||
escalation_enabled=True,
|
||||
escalation_threshold_percent=85.0,
|
||||
)
|
||||
```
|
||||
|
||||
#### Escalation Policies
|
||||
|
||||
```python
|
||||
# Critical escalation policy
|
||||
escalation_policy = EscalationPolicy.objects.create(
|
||||
name='Critical Incident Escalation',
|
||||
description='Escalation for critical incidents',
|
||||
escalation_type='TIME_BASED',
|
||||
trigger_condition='SLA_THRESHOLD',
|
||||
incident_severities=['CRITICAL', 'EMERGENCY'],
|
||||
trigger_delay_minutes=0,
|
||||
escalation_steps=[
|
||||
{
|
||||
'level': 1,
|
||||
'delay_minutes': 5,
|
||||
'actions': ['notify_oncall', 'notify_manager'],
|
||||
'channels': ['email', 'sms']
|
||||
},
|
||||
{
|
||||
'level': 2,
|
||||
'delay_minutes': 15,
|
||||
'actions': ['notify_director', 'page_oncall'],
|
||||
'channels': ['email', 'sms', 'phone']
|
||||
},
|
||||
{
|
||||
'level': 3,
|
||||
'delay_minutes': 30,
|
||||
'actions': ['notify_executive', 'escalate_to_vendor'],
|
||||
'channels': ['email', 'phone', 'webhook']
|
||||
}
|
||||
],
|
||||
notification_channels=['email', 'sms', 'phone'],
|
||||
)
|
||||
```
|
||||
|
||||
#### On-Call Rotations
|
||||
|
||||
```python
|
||||
# Weekly rotation
|
||||
rotation = OnCallRotation.objects.create(
|
||||
name='Primary On-Call Rotation',
|
||||
description='Primary rotation for incident response',
|
||||
rotation_type='WEEKLY',
|
||||
team_name='Incident Response Team',
|
||||
schedule_config={
|
||||
'rotation_length_days': 7,
|
||||
'handoff_time': '09:00',
|
||||
'timezone': 'UTC'
|
||||
},
|
||||
timezone='UTC',
|
||||
)
|
||||
|
||||
# Create assignments
|
||||
assignment = OnCallAssignment.objects.create(
|
||||
rotation=rotation,
|
||||
user=user,
|
||||
start_time=timezone.now(),
|
||||
end_time=timezone.now() + timedelta(days=7),
|
||||
status='ACTIVE'
|
||||
)
|
||||
```
|
||||
|
||||
## Integration with Other Modules
|
||||
|
||||
### Incident Intelligence Integration
|
||||
|
||||
The SLA module automatically creates SLA instances when incidents are created:
|
||||
|
||||
```python
|
||||
# When an incident is created, applicable SLA definitions are found
|
||||
# and SLA instances are automatically created
|
||||
incident = Incident.objects.create(
|
||||
title='Database Connection Failure',
|
||||
description='Unable to connect to primary database',
|
||||
severity='CRITICAL',
|
||||
category='DATABASE',
|
||||
reporter=user,
|
||||
)
|
||||
|
||||
# This automatically triggers SLA instance creation via signals
|
||||
```
|
||||
|
||||
### Automation Orchestration Integration
|
||||
|
||||
SLA breaches can trigger automation workflows:
|
||||
|
||||
```python
|
||||
# In automation_orchestration models, you can reference SLA instances
|
||||
class RunbookExecution(models.Model):
|
||||
# ... existing fields ...
|
||||
sla_instance = models.ForeignKey(
|
||||
'sla_oncall.SLAInstance',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='runbook_executions'
|
||||
)
|
||||
```
|
||||
|
||||
### Security Integration
|
||||
|
||||
On-call assignments respect security clearances:
|
||||
|
||||
```python
|
||||
# Users with appropriate clearance levels can be assigned to sensitive incidents
|
||||
if user.clearance_level.level >= incident.get_required_clearance_level():
|
||||
# User can be assigned to this incident
|
||||
assignment = OnCallAssignment.objects.create(...)
|
||||
```
|
||||
|
||||
## Monitoring and Alerting
|
||||
|
||||
### SLA Breach Monitoring
|
||||
|
||||
Monitor SLA instances for breaches:
|
||||
|
||||
```python
|
||||
# Get all breached SLAs
|
||||
breached_slas = SLAInstance.objects.filter(status='BREACHED')
|
||||
|
||||
# Get SLAs at risk (within 15 minutes of breach)
|
||||
warning_time = timezone.now() + timedelta(minutes=15)
|
||||
at_risk_slas = SLAInstance.objects.filter(
|
||||
status='ACTIVE',
|
||||
target_time__lte=warning_time
|
||||
)
|
||||
```
|
||||
|
||||
### Escalation Monitoring
|
||||
|
||||
Monitor active escalations:
|
||||
|
||||
```python
|
||||
# Get all active escalations
|
||||
active_escalations = EscalationInstance.objects.filter(
|
||||
status__in=['PENDING', 'TRIGGERED']
|
||||
)
|
||||
|
||||
# Get escalations by level
|
||||
level_2_escalations = EscalationInstance.objects.filter(
|
||||
escalation_level=2,
|
||||
status='TRIGGERED'
|
||||
)
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
Track on-call performance:
|
||||
|
||||
```python
|
||||
# Get current on-call assignments
|
||||
current_assignments = OnCallAssignment.objects.filter(
|
||||
status='ACTIVE',
|
||||
start_time__lte=timezone.now(),
|
||||
end_time__gte=timezone.now()
|
||||
)
|
||||
|
||||
# Calculate average response time
|
||||
avg_response_time = current_assignments.aggregate(
|
||||
avg_response=Avg('response_time_avg')
|
||||
)['avg_response']
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### SLA Definition Best Practices
|
||||
|
||||
1. **Start Simple**: Begin with basic SLAs and add complexity as needed
|
||||
2. **Business Hours Consideration**: Use business hours for non-critical incidents
|
||||
3. **Escalation Thresholds**: Set escalation thresholds at 75-85% of SLA time
|
||||
4. **Regular Review**: Review and adjust SLAs based on performance data
|
||||
|
||||
### On-Call Management Best Practices
|
||||
|
||||
1. **Clear Handoffs**: Use structured handoff processes with notes
|
||||
2. **Rotation Length**: Keep rotations between 1-2 weeks for optimal coverage
|
||||
3. **Backup Coverage**: Always have backup on-call personnel
|
||||
4. **Training**: Ensure on-call personnel are properly trained
|
||||
|
||||
### Escalation Best Practices
|
||||
|
||||
1. **Progressive Escalation**: Use multiple levels with increasing urgency
|
||||
2. **Clear Actions**: Define specific actions for each escalation level
|
||||
3. **Multiple Channels**: Use multiple notification channels for critical escalations
|
||||
4. **Documentation**: Document all escalation actions and outcomes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **SLA Not Created**: Check if SLA definition criteria match incident attributes
|
||||
2. **Escalation Not Triggered**: Verify escalation policy is active and criteria match
|
||||
3. **On-Call Not Found**: Ensure active assignments exist for the rotation
|
||||
4. **Business Hours Issues**: Verify timezone configuration and business hours setup
|
||||
|
||||
### Debugging Commands
|
||||
|
||||
```bash
|
||||
# Check SLA instances for a specific incident
|
||||
python manage.py shell
|
||||
>>> incident = Incident.objects.get(id='incident-id')
|
||||
>>> sla_instances = incident.sla_instances.all()
|
||||
>>> for sla in sla_instances:
|
||||
... print(f"SLA: {sla.sla_definition.name}, Status: {sla.status}")
|
||||
|
||||
# Check current on-call for a rotation
|
||||
>>> rotation = OnCallRotation.objects.get(id='rotation-id')
|
||||
>>> current = rotation.get_current_oncall()
|
||||
>>> print(f"Current on-call: {current.user.username if current else 'None'}")
|
||||
|
||||
# Check business hours
|
||||
>>> business_hours = BusinessHours.objects.get(id='business-hours-id')
|
||||
>>> now = timezone.now()
|
||||
>>> print(f"Is business hours: {business_hours.is_business_hours(now)}")
|
||||
```
|
||||
0
ETB-API/sla_oncall/__init__.py
Normal file
0
ETB-API/sla_oncall/__init__.py
Normal file
BIN
ETB-API/sla_oncall/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/admin.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/apps.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/models.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/signals.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/signals.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/tests.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/tests.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/__pycache__/urls.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
293
ETB-API/sla_oncall/admin.py
Normal file
293
ETB-API/sla_oncall/admin.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Admin configuration for SLA & On-Call Management
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from .models import (
|
||||
BusinessHours,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
SLAInstance,
|
||||
EscalationInstance,
|
||||
NotificationTemplate,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(BusinessHours)
|
||||
class BusinessHoursAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'timezone', 'weekday_start', 'weekday_end', 'is_active', 'is_default']
|
||||
list_filter = ['is_active', 'is_default', 'timezone']
|
||||
search_fields = ['name', 'description']
|
||||
ordering = ['name']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'description', 'timezone')
|
||||
}),
|
||||
('Business Hours', {
|
||||
'fields': ('weekday_start', 'weekday_end', 'weekend_start', 'weekend_end')
|
||||
}),
|
||||
('Overrides', {
|
||||
'fields': ('day_overrides', 'holiday_calendar'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(SLADefinition)
|
||||
class SLADefinitionAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'sla_type', 'target_duration_minutes',
|
||||
'business_hours_only', 'is_active', 'is_default'
|
||||
]
|
||||
list_filter = ['sla_type', 'business_hours_only', 'is_active', 'is_default']
|
||||
search_fields = ['name', 'description']
|
||||
ordering = ['name']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'description', 'sla_type')
|
||||
}),
|
||||
('Targeting Criteria', {
|
||||
'fields': ('incident_categories', 'incident_severities', 'incident_priorities')
|
||||
}),
|
||||
('SLA Configuration', {
|
||||
'fields': ('target_duration_minutes', 'business_hours_only', 'business_hours')
|
||||
}),
|
||||
('Escalation', {
|
||||
'fields': ('escalation_enabled', 'escalation_threshold_percent')
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(EscalationPolicy)
|
||||
class EscalationPolicyAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'escalation_type', 'trigger_condition',
|
||||
'trigger_delay_minutes', 'is_active'
|
||||
]
|
||||
list_filter = ['escalation_type', 'trigger_condition', 'is_active']
|
||||
search_fields = ['name', 'description']
|
||||
ordering = ['name']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'description', 'escalation_type', 'trigger_condition')
|
||||
}),
|
||||
('Targeting', {
|
||||
'fields': ('incident_severities', 'incident_categories')
|
||||
}),
|
||||
('Configuration', {
|
||||
'fields': ('trigger_delay_minutes', 'escalation_steps')
|
||||
}),
|
||||
('Notifications', {
|
||||
'fields': ('notification_channels', 'notification_templates'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(OnCallRotation)
|
||||
class OnCallRotationAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'rotation_type', 'team_name', 'external_system', 'status'
|
||||
]
|
||||
list_filter = ['rotation_type', 'external_system', 'status']
|
||||
search_fields = ['name', 'team_name', 'description']
|
||||
ordering = ['name']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'description', 'rotation_type', 'status')
|
||||
}),
|
||||
('Team Configuration', {
|
||||
'fields': ('team_name', 'team_description')
|
||||
}),
|
||||
('Schedule', {
|
||||
'fields': ('schedule_config', 'timezone')
|
||||
}),
|
||||
('External Integration', {
|
||||
'fields': ('external_system', 'external_system_id', 'integration_config'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(OnCallAssignment)
|
||||
class OnCallAssignmentAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'user', 'rotation', 'start_time', 'end_time', 'status', 'incidents_handled'
|
||||
]
|
||||
list_filter = ['status', 'rotation', 'start_time']
|
||||
search_fields = ['user__username', 'user__email', 'rotation__name']
|
||||
ordering = ['-start_time']
|
||||
|
||||
fieldsets = (
|
||||
('Assignment Details', {
|
||||
'fields': ('rotation', 'user', 'start_time', 'end_time', 'status')
|
||||
}),
|
||||
('Handoff Information', {
|
||||
'fields': ('handoff_notes', 'handed_off_from', 'handoff_time'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Performance', {
|
||||
'fields': ('incidents_handled', 'response_time_avg')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(SLAInstance)
|
||||
class SLAInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'incident', 'sla_definition', 'status', 'target_time',
|
||||
'escalation_triggered', 'time_remaining_display'
|
||||
]
|
||||
list_filter = ['status', 'escalation_triggered', 'sla_definition__sla_type']
|
||||
search_fields = ['incident__title', 'sla_definition__name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('SLA Details', {
|
||||
'fields': ('sla_definition', 'incident', 'status', 'target_time')
|
||||
}),
|
||||
('Timing', {
|
||||
'fields': ('started_at', 'met_at', 'breached_at')
|
||||
}),
|
||||
('Escalation', {
|
||||
'fields': ('escalation_policy', 'escalation_triggered',
|
||||
'escalation_triggered_at', 'escalation_level')
|
||||
}),
|
||||
('Performance', {
|
||||
'fields': ('response_time', 'resolution_time')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'started_at']
|
||||
|
||||
def time_remaining_display(self, obj):
|
||||
"""Display time remaining with color coding"""
|
||||
if obj.status == 'ACTIVE':
|
||||
remaining = obj.time_remaining
|
||||
if remaining > timedelta(hours=1):
|
||||
color = 'green'
|
||||
elif remaining > timedelta(minutes=15):
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'red'
|
||||
|
||||
return format_html(
|
||||
'<span style="color: {};">{}</span>',
|
||||
color,
|
||||
str(remaining).split('.')[0] if remaining else 'Expired'
|
||||
)
|
||||
return '-'
|
||||
time_remaining_display.short_description = 'Time Remaining'
|
||||
|
||||
|
||||
@admin.register(EscalationInstance)
|
||||
class EscalationInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'incident', 'escalation_policy', 'status', 'escalation_level',
|
||||
'triggered_at', 'current_step'
|
||||
]
|
||||
list_filter = ['status', 'escalation_level', 'escalation_policy__escalation_type']
|
||||
search_fields = ['incident__title', 'escalation_policy__name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Escalation Details', {
|
||||
'fields': ('escalation_policy', 'incident', 'sla_instance', 'status')
|
||||
}),
|
||||
('Progress', {
|
||||
'fields': ('escalation_level', 'current_step')
|
||||
}),
|
||||
('Timing', {
|
||||
'fields': ('triggered_at', 'acknowledged_at', 'resolved_at')
|
||||
}),
|
||||
('Actions', {
|
||||
'fields': ('notifications_sent', 'actions_taken'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
@admin.register(NotificationTemplate)
|
||||
class NotificationTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'template_type', 'channel_type', 'is_active', 'is_default'
|
||||
]
|
||||
list_filter = ['template_type', 'channel_type', 'is_active', 'is_default']
|
||||
search_fields = ['name', 'subject_template']
|
||||
ordering = ['template_type', 'channel_type', 'name']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'template_type', 'channel_type')
|
||||
}),
|
||||
('Template Content', {
|
||||
'fields': ('subject_template', 'body_template', 'variables')
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
14
ETB-API/sla_oncall/apps.py
Normal file
14
ETB-API/sla_oncall/apps.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SlaOncallConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'sla_oncall'
|
||||
verbose_name = 'SLA & On-Call Management'
|
||||
|
||||
def ready(self):
|
||||
"""Import signal handlers when the app is ready"""
|
||||
try:
|
||||
import sla_oncall.signals
|
||||
except ImportError:
|
||||
pass
|
||||
1
ETB-API/sla_oncall/management/__init__.py
Normal file
1
ETB-API/sla_oncall/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management commands for SLA & On-Call Management
|
||||
Binary file not shown.
1
ETB-API/sla_oncall/management/commands/__init__.py
Normal file
1
ETB-API/sla_oncall/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management commands
|
||||
Binary file not shown.
Binary file not shown.
395
ETB-API/sla_oncall/management/commands/setup_sla_oncall.py
Normal file
395
ETB-API/sla_oncall/management/commands/setup_sla_oncall.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Management command to set up SLA & On-Call Management with default configurations
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
from datetime import time, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from sla_oncall.models import (
|
||||
BusinessHours,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
NotificationTemplate,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Set up SLA & On-Call Management with default configurations'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Force recreation of existing configurations',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Setting up SLA & On-Call Management...')
|
||||
|
||||
force = options['force']
|
||||
|
||||
# Create default business hours
|
||||
self.create_default_business_hours(force)
|
||||
|
||||
# Create default SLA definitions
|
||||
self.create_default_sla_definitions(force)
|
||||
|
||||
# Create default escalation policies
|
||||
self.create_default_escalation_policies(force)
|
||||
|
||||
# Create default notification templates
|
||||
self.create_default_notification_templates(force)
|
||||
|
||||
# Create sample on-call rotation (if users exist)
|
||||
self.create_sample_oncall_rotation(force)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Successfully set up SLA & On-Call Management!')
|
||||
)
|
||||
|
||||
def create_default_business_hours(self, force):
|
||||
"""Create default business hours configurations"""
|
||||
self.stdout.write('Creating default business hours...')
|
||||
|
||||
default_hours, created = BusinessHours.objects.get_or_create(
|
||||
name='Standard Business Hours',
|
||||
defaults={
|
||||
'description': 'Standard 9-5 business hours, Monday to Friday',
|
||||
'timezone': 'UTC',
|
||||
'weekday_start': time(9, 0),
|
||||
'weekday_end': time(17, 0),
|
||||
'weekend_start': time(10, 0),
|
||||
'weekend_end': time(16, 0),
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {default_hours.name}')
|
||||
|
||||
# 24/7 business hours
|
||||
twenty_four_seven, created = BusinessHours.objects.get_or_create(
|
||||
name='24/7 Operations',
|
||||
defaults={
|
||||
'description': '24/7 operations - always business hours',
|
||||
'timezone': 'UTC',
|
||||
'weekday_start': time(0, 0),
|
||||
'weekday_end': time(23, 59),
|
||||
'weekend_start': time(0, 0),
|
||||
'weekend_end': time(23, 59),
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {twenty_four_seven.name}')
|
||||
|
||||
def create_default_sla_definitions(self, force):
|
||||
"""Create default SLA definitions"""
|
||||
self.stdout.write('Creating default SLA definitions...')
|
||||
|
||||
# Critical incidents SLA
|
||||
critical_sla, created = SLADefinition.objects.get_or_create(
|
||||
name='Critical Incident Response',
|
||||
defaults={
|
||||
'description': 'SLA for critical and emergency incidents',
|
||||
'sla_type': 'RESPONSE_TIME',
|
||||
'incident_severities': ['CRITICAL', 'EMERGENCY'],
|
||||
'incident_priorities': ['P1'],
|
||||
'target_duration_minutes': 15,
|
||||
'business_hours_only': False,
|
||||
'escalation_enabled': True,
|
||||
'escalation_threshold_percent': 75.0,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {critical_sla.name}')
|
||||
|
||||
# High priority incidents SLA
|
||||
high_sla, created = SLADefinition.objects.get_or_create(
|
||||
name='High Priority Response',
|
||||
defaults={
|
||||
'description': 'SLA for high priority incidents',
|
||||
'sla_type': 'RESPONSE_TIME',
|
||||
'incident_severities': ['HIGH'],
|
||||
'incident_priorities': ['P2'],
|
||||
'target_duration_minutes': 30,
|
||||
'business_hours_only': False,
|
||||
'escalation_enabled': True,
|
||||
'escalation_threshold_percent': 80.0,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {high_sla.name}')
|
||||
|
||||
# Medium priority incidents SLA
|
||||
medium_sla, created = SLADefinition.objects.get_or_create(
|
||||
name='Medium Priority Response',
|
||||
defaults={
|
||||
'description': 'SLA for medium priority incidents during business hours',
|
||||
'sla_type': 'RESPONSE_TIME',
|
||||
'incident_severities': ['MEDIUM'],
|
||||
'incident_priorities': ['P3'],
|
||||
'target_duration_minutes': 120,
|
||||
'business_hours_only': True,
|
||||
'escalation_enabled': True,
|
||||
'escalation_threshold_percent': 85.0,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {medium_sla.name}')
|
||||
|
||||
# Resolution time SLA
|
||||
resolution_sla, created = SLADefinition.objects.get_or_create(
|
||||
name='Critical Incident Resolution',
|
||||
defaults={
|
||||
'description': 'SLA for resolving critical incidents',
|
||||
'sla_type': 'RESOLUTION_TIME',
|
||||
'incident_severities': ['CRITICAL', 'EMERGENCY'],
|
||||
'incident_priorities': ['P1'],
|
||||
'target_duration_minutes': 240, # 4 hours
|
||||
'business_hours_only': False,
|
||||
'escalation_enabled': True,
|
||||
'escalation_threshold_percent': 90.0,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {resolution_sla.name}')
|
||||
|
||||
def create_default_escalation_policies(self, force):
|
||||
"""Create default escalation policies"""
|
||||
self.stdout.write('Creating default escalation policies...')
|
||||
|
||||
# Critical escalation policy
|
||||
critical_escalation, created = EscalationPolicy.objects.get_or_create(
|
||||
name='Critical Incident Escalation',
|
||||
defaults={
|
||||
'description': 'Escalation policy for critical and emergency incidents',
|
||||
'escalation_type': 'TIME_BASED',
|
||||
'trigger_condition': 'SLA_THRESHOLD',
|
||||
'incident_severities': ['CRITICAL', 'EMERGENCY'],
|
||||
'trigger_delay_minutes': 0,
|
||||
'escalation_steps': [
|
||||
{
|
||||
'level': 1,
|
||||
'delay_minutes': 5,
|
||||
'actions': ['notify_oncall', 'notify_manager'],
|
||||
'channels': ['email', 'sms']
|
||||
},
|
||||
{
|
||||
'level': 2,
|
||||
'delay_minutes': 15,
|
||||
'actions': ['notify_director', 'page_oncall'],
|
||||
'channels': ['email', 'sms', 'phone']
|
||||
},
|
||||
{
|
||||
'level': 3,
|
||||
'delay_minutes': 30,
|
||||
'actions': ['notify_executive', 'escalate_to_vendor'],
|
||||
'channels': ['email', 'phone', 'webhook']
|
||||
}
|
||||
],
|
||||
'notification_channels': ['email', 'sms', 'phone'],
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {critical_escalation.name}')
|
||||
|
||||
# Standard escalation policy
|
||||
standard_escalation, created = EscalationPolicy.objects.get_or_create(
|
||||
name='Standard Escalation',
|
||||
defaults={
|
||||
'description': 'Standard escalation policy for most incidents',
|
||||
'escalation_type': 'TIME_BASED',
|
||||
'trigger_condition': 'SLA_THRESHOLD',
|
||||
'incident_severities': ['LOW', 'MEDIUM', 'HIGH'],
|
||||
'trigger_delay_minutes': 5,
|
||||
'escalation_steps': [
|
||||
{
|
||||
'level': 1,
|
||||
'delay_minutes': 15,
|
||||
'actions': ['notify_oncall'],
|
||||
'channels': ['email']
|
||||
},
|
||||
{
|
||||
'level': 2,
|
||||
'delay_minutes': 30,
|
||||
'actions': ['notify_manager'],
|
||||
'channels': ['email', 'sms']
|
||||
}
|
||||
],
|
||||
'notification_channels': ['email', 'sms'],
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {standard_escalation.name}')
|
||||
|
||||
def create_default_notification_templates(self, force):
|
||||
"""Create default notification templates"""
|
||||
self.stdout.write('Creating default notification templates...')
|
||||
|
||||
# Email escalation template
|
||||
email_escalation, created = NotificationTemplate.objects.get_or_create(
|
||||
name='Email Escalation Alert',
|
||||
template_type='ESCALATION',
|
||||
channel_type='EMAIL',
|
||||
defaults={
|
||||
'subject_template': 'URGENT: Incident #{incident_id} Escalated - {incident_title}',
|
||||
'body_template': '''
|
||||
Incident #{incident_id} has been escalated to Level {escalation_level}.
|
||||
|
||||
Details:
|
||||
- Title: {incident_title}
|
||||
- Severity: {incident_severity}
|
||||
- Status: {incident_status}
|
||||
- Created: {incident_created_at}
|
||||
- SLA Target: {sla_target_time}
|
||||
- Current On-Call: {current_oncall}
|
||||
|
||||
Please respond immediately.
|
||||
|
||||
View incident: {incident_url}
|
||||
''',
|
||||
'variables': [
|
||||
'incident_id', 'incident_title', 'incident_severity',
|
||||
'incident_status', 'incident_created_at', 'sla_target_time',
|
||||
'current_oncall', 'incident_url', 'escalation_level'
|
||||
],
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {email_escalation.name}')
|
||||
|
||||
# SMS escalation template
|
||||
sms_escalation, created = NotificationTemplate.objects.get_or_create(
|
||||
name='SMS Escalation Alert',
|
||||
template_type='ESCALATION',
|
||||
channel_type='SMS',
|
||||
defaults={
|
||||
'subject_template': '',
|
||||
'body_template': 'URGENT: Incident #{incident_id} escalated to L{escalation_level}. {incident_title}. Respond now.',
|
||||
'variables': ['incident_id', 'incident_title', 'escalation_level'],
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {sms_escalation.name}')
|
||||
|
||||
# SLA breach template
|
||||
sla_breach, created = NotificationTemplate.objects.get_or_create(
|
||||
name='SLA Breach Alert',
|
||||
template_type='SLA_BREACH',
|
||||
channel_type='EMAIL',
|
||||
defaults={
|
||||
'subject_template': 'SLA BREACH: Incident #{incident_id} - {incident_title}',
|
||||
'body_template': '''
|
||||
SLA BREACH ALERT
|
||||
|
||||
The SLA for incident #{incident_id} has been breached.
|
||||
|
||||
Details:
|
||||
- Incident: {incident_title}
|
||||
- SLA Type: {sla_type}
|
||||
- Target Time: {sla_target_time}
|
||||
- Breach Time: {breach_time}
|
||||
- Breach Duration: {breach_duration}
|
||||
|
||||
Immediate action required.
|
||||
''',
|
||||
'variables': [
|
||||
'incident_id', 'incident_title', 'sla_type',
|
||||
'sla_target_time', 'breach_time', 'breach_duration'
|
||||
],
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {sla_breach.name}')
|
||||
|
||||
def create_sample_oncall_rotation(self, force):
|
||||
"""Create a sample on-call rotation if users exist"""
|
||||
self.stdout.write('Creating sample on-call rotation...')
|
||||
|
||||
# Check if we have any users
|
||||
if not User.objects.exists():
|
||||
self.stdout.write(' ⚠ No users found. Skipping on-call rotation creation.')
|
||||
return
|
||||
|
||||
# Get first user as admin
|
||||
admin_user = User.objects.first()
|
||||
|
||||
# Create sample rotation
|
||||
sample_rotation, created = OnCallRotation.objects.get_or_create(
|
||||
name='Primary On-Call Rotation',
|
||||
defaults={
|
||||
'description': 'Primary on-call rotation for incident response',
|
||||
'rotation_type': 'WEEKLY',
|
||||
'status': 'ACTIVE',
|
||||
'team_name': 'Incident Response Team',
|
||||
'team_description': 'Primary team responsible for incident response',
|
||||
'schedule_config': {
|
||||
'rotation_length_days': 7,
|
||||
'handoff_time': '09:00',
|
||||
'timezone': 'UTC'
|
||||
},
|
||||
'timezone': 'UTC',
|
||||
'external_system': 'INTERNAL',
|
||||
'created_by': admin_user,
|
||||
}
|
||||
)
|
||||
|
||||
if created or force:
|
||||
self.stdout.write(f' ✓ Created/Updated: {sample_rotation.name}')
|
||||
|
||||
# Create a sample assignment for the next week
|
||||
if User.objects.count() >= 2:
|
||||
users = list(User.objects.all()[:2])
|
||||
start_time = timezone.now()
|
||||
end_time = start_time + timedelta(days=7)
|
||||
|
||||
assignment, created = OnCallAssignment.objects.get_or_create(
|
||||
rotation=sample_rotation,
|
||||
user=users[0],
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
defaults={
|
||||
'status': 'ACTIVE',
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
self.stdout.write(f' ✓ Created assignment for {users[0].username}')
|
||||
else:
|
||||
self.stdout.write(f' ⚠ Rotation already exists: {sample_rotation.name}')
|
||||
283
ETB-API/sla_oncall/migrations/0001_initial.py
Normal file
283
ETB-API/sla_oncall/migrations/0001_initial.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-18 15:50
|
||||
|
||||
import datetime
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('incident_intelligence', '0003_incident_auto_remediation_attempted_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BusinessHours',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200, unique=True)),
|
||||
('description', models.TextField()),
|
||||
('timezone', models.CharField(default='UTC', max_length=50)),
|
||||
('weekday_start', models.TimeField(default=datetime.time(9, 0))),
|
||||
('weekday_end', models.TimeField(default=datetime.time(17, 0))),
|
||||
('weekend_start', models.TimeField(default=datetime.time(10, 0))),
|
||||
('weekend_end', models.TimeField(default=datetime.time(16, 0))),
|
||||
('day_overrides', models.JSONField(default=dict, help_text='Override hours for specific dates (YYYY-MM-DD format)')),
|
||||
('holiday_calendar', models.JSONField(default=list, help_text="List of holidays (YYYY-MM-DD format) when business hours don't apply")),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_default', models.BooleanField(default=False, help_text='Default business hours for the system')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EscalationPolicy',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200, unique=True)),
|
||||
('description', models.TextField()),
|
||||
('escalation_type', models.CharField(choices=[('TIME_BASED', 'Time-based Escalation'), ('SEVERITY_BASED', 'Severity-based Escalation'), ('RESOURCE_BASED', 'Resource-based Escalation'), ('CUSTOM', 'Custom Escalation')], max_length=20)),
|
||||
('trigger_condition', models.CharField(choices=[('SLA_BREACH', 'SLA Breach'), ('SLA_THRESHOLD', 'SLA Threshold Reached'), ('NO_RESPONSE', 'No Response'), ('NO_ACKNOWLEDGMENT', 'No Acknowledgment'), ('CUSTOM', 'Custom Condition')], max_length=20)),
|
||||
('incident_severities', models.JSONField(default=list, help_text='List of incident severities this policy applies to')),
|
||||
('incident_categories', models.JSONField(default=list, help_text='List of incident categories this policy applies to')),
|
||||
('trigger_delay_minutes', models.PositiveIntegerField(default=0, help_text='Delay before escalation triggers (in minutes)')),
|
||||
('escalation_steps', models.JSONField(default=list, help_text='List of escalation steps with timing and actions')),
|
||||
('notification_channels', models.JSONField(default=list, help_text='Channels to notify during escalation (email, sms, slack, etc.)')),
|
||||
('notification_templates', models.JSONField(default=dict, help_text='Templates for different notification channels')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationTemplate',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('template_type', models.CharField(choices=[('ESCALATION', 'Escalation Notification'), ('ONCALL_HANDOFF', 'On-Call Handoff'), ('SLA_BREACH', 'SLA Breach Alert'), ('SLA_WARNING', 'SLA Warning'), ('CUSTOM', 'Custom Notification')], max_length=20)),
|
||||
('channel_type', models.CharField(choices=[('EMAIL', 'Email'), ('SMS', 'SMS'), ('SLACK', 'Slack'), ('TEAMS', 'Microsoft Teams'), ('WEBHOOK', 'Webhook'), ('CUSTOM', 'Custom Channel')], max_length=20)),
|
||||
('subject_template', models.CharField(help_text='Subject template with variables', max_length=500)),
|
||||
('body_template', models.TextField(help_text='Body template with variables')),
|
||||
('variables', models.JSONField(default=list, help_text='Available variables for this template')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['template_type', 'channel_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OnCallRotation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200, unique=True)),
|
||||
('description', models.TextField()),
|
||||
('rotation_type', models.CharField(choices=[('WEEKLY', 'Weekly Rotation'), ('DAILY', 'Daily Rotation'), ('MONTHLY', 'Monthly Rotation'), ('CUSTOM', 'Custom Schedule')], max_length=20)),
|
||||
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('PAUSED', 'Paused'), ('INACTIVE', 'Inactive')], default='ACTIVE', max_length=20)),
|
||||
('team_name', models.CharField(max_length=100)),
|
||||
('team_description', models.TextField(blank=True, null=True)),
|
||||
('schedule_config', models.JSONField(default=dict, help_text='Configuration for the rotation schedule')),
|
||||
('timezone', models.CharField(default='UTC', max_length=50)),
|
||||
('external_system', models.CharField(choices=[('PAGERDUTY', 'PagerDuty'), ('OPSGENIE', 'OpsGenie'), ('INTERNAL', 'Internal System'), ('CUSTOM', 'Custom Integration')], default='INTERNAL', max_length=50)),
|
||||
('external_system_id', models.CharField(blank=True, help_text='ID in external system (PagerDuty schedule ID, etc.)', max_length=255, null=True)),
|
||||
('integration_config', models.JSONField(default=dict, help_text='Configuration for external system integration')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OnCallAssignment',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('start_time', models.DateTimeField()),
|
||||
('end_time', models.DateTimeField()),
|
||||
('status', models.CharField(choices=[('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled')], default='SCHEDULED', max_length=20)),
|
||||
('handoff_notes', models.TextField(blank=True, null=True)),
|
||||
('handoff_time', models.DateTimeField(blank=True, null=True)),
|
||||
('incidents_handled', models.PositiveIntegerField(default=0)),
|
||||
('response_time_avg', models.DurationField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('handed_off_from', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='handed_off_assignments', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('rotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='sla_oncall.oncallrotation')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['start_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SLADefinition',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('sla_type', models.CharField(choices=[('RESPONSE_TIME', 'Response Time'), ('RESOLUTION_TIME', 'Resolution Time'), ('ACKNOWLEDGMENT_TIME', 'Acknowledgment Time'), ('FIRST_RESPONSE', 'First Response Time')], max_length=20)),
|
||||
('incident_categories', models.JSONField(default=list, help_text='List of incident categories this SLA applies to')),
|
||||
('incident_severities', models.JSONField(default=list, help_text='List of incident severities this SLA applies to')),
|
||||
('incident_priorities', models.JSONField(default=list, help_text='List of incident priorities this SLA applies to')),
|
||||
('target_duration_minutes', models.PositiveIntegerField(help_text='SLA target in minutes')),
|
||||
('business_hours_only', models.BooleanField(default=False, help_text='Whether SLA only applies during business hours')),
|
||||
('escalation_enabled', models.BooleanField(default=True)),
|
||||
('escalation_threshold_percent', models.FloatField(default=80.0, help_text='Escalate when X% of SLA time has passed', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100.0)])),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('business_hours', models.ForeignKey(blank=True, help_text='Business hours configuration for this SLA', null=True, on_delete=django.db.models.deletion.SET_NULL, to='sla_oncall.businesshours')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SLAInstance',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('MET', 'SLA Met'), ('BREACHED', 'SLA Breached'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20)),
|
||||
('target_time', models.DateTimeField(help_text='When the SLA should be met')),
|
||||
('started_at', models.DateTimeField(auto_now_add=True)),
|
||||
('met_at', models.DateTimeField(blank=True, null=True)),
|
||||
('breached_at', models.DateTimeField(blank=True, null=True)),
|
||||
('escalation_triggered', models.BooleanField(default=False)),
|
||||
('escalation_triggered_at', models.DateTimeField(blank=True, null=True)),
|
||||
('escalation_level', models.PositiveIntegerField(default=0)),
|
||||
('response_time', models.DurationField(blank=True, null=True)),
|
||||
('resolution_time', models.DurationField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('escalation_policy', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sla_oncall.escalationpolicy')),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sla_instances', to='incident_intelligence.incident')),
|
||||
('sla_definition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sla_oncall.sladefinition')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EscalationInstance',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('status', models.CharField(choices=[('PENDING', 'Pending'), ('TRIGGERED', 'Triggered'), ('ACKNOWLEDGED', 'Acknowledged'), ('RESOLVED', 'Resolved'), ('CANCELLED', 'Cancelled')], default='PENDING', max_length=20)),
|
||||
('escalation_level', models.PositiveIntegerField(default=1)),
|
||||
('current_step', models.PositiveIntegerField(default=0)),
|
||||
('triggered_at', models.DateTimeField(blank=True, null=True)),
|
||||
('acknowledged_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('notifications_sent', models.JSONField(default=list, help_text='List of notifications sent during escalation')),
|
||||
('actions_taken', models.JSONField(default=list, help_text='List of actions taken during escalation')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='escalation_instances', to='incident_intelligence.incident')),
|
||||
('escalation_policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sla_oncall.escalationpolicy')),
|
||||
('sla_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='escalation_instances', to='sla_oncall.slainstance')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='businesshours',
|
||||
index=models.Index(fields=['is_active', 'is_default'], name='sla_oncall__is_acti_5700c2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='escalationpolicy',
|
||||
index=models.Index(fields=['escalation_type', 'is_active'], name='sla_oncall__escalat_f6341c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='escalationpolicy',
|
||||
index=models.Index(fields=['trigger_condition'], name='sla_oncall__trigger_75de52_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notificationtemplate',
|
||||
index=models.Index(fields=['template_type', 'channel_type'], name='sla_oncall__templat_a3daf6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notificationtemplate',
|
||||
index=models.Index(fields=['is_active'], name='sla_oncall__is_acti_cb24d6_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='notificationtemplate',
|
||||
unique_together={('template_type', 'channel_type', 'name')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='oncallrotation',
|
||||
index=models.Index(fields=['status', 'rotation_type'], name='sla_oncall__status_0522d6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='oncallrotation',
|
||||
index=models.Index(fields=['external_system'], name='sla_oncall__externa_ad3fd1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='oncallassignment',
|
||||
index=models.Index(fields=['rotation', 'start_time'], name='sla_oncall__rotatio_2db363_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='oncallassignment',
|
||||
index=models.Index(fields=['user', 'start_time'], name='sla_oncall__user_id_0776dc_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='oncallassignment',
|
||||
index=models.Index(fields=['status', 'start_time'], name='sla_oncall__status_3bec52_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='sladefinition',
|
||||
index=models.Index(fields=['sla_type', 'is_active'], name='sla_oncall__sla_typ_f69c8d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='sladefinition',
|
||||
index=models.Index(fields=['incident_severities'], name='sla_oncall__inciden_ba4baa_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='sladefinition',
|
||||
index=models.Index(fields=['incident_categories'], name='sla_oncall__inciden_da3c38_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='slainstance',
|
||||
index=models.Index(fields=['incident', 'status'], name='sla_oncall__inciden_723905_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='slainstance',
|
||||
index=models.Index(fields=['sla_definition', 'status'], name='sla_oncall__sla_def_57544b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='slainstance',
|
||||
index=models.Index(fields=['target_time', 'status'], name='sla_oncall__target__b6b252_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='escalationinstance',
|
||||
index=models.Index(fields=['incident', 'status'], name='sla_oncall__inciden_58c9bd_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='escalationinstance',
|
||||
index=models.Index(fields=['escalation_policy', 'status'], name='sla_oncall__escalat_f748a6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='escalationinstance',
|
||||
index=models.Index(fields=['triggered_at'], name='sla_oncall__trigger_a8b2b2_idx'),
|
||||
),
|
||||
]
|
||||
0
ETB-API/sla_oncall/migrations/__init__.py
Normal file
0
ETB-API/sla_oncall/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
653
ETB-API/sla_oncall/models.py
Normal file
653
ETB-API/sla_oncall/models.py
Normal file
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
SLA & On-Call Enhancement models for Enterprise Incident Management API
|
||||
Implements dynamic SLAs, escalation policies, on-call rotations, and business hours management
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime, timedelta, time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BusinessHours(models.Model):
|
||||
"""Business hours configuration for different teams and services"""
|
||||
|
||||
WEEKDAYS = [
|
||||
('MONDAY', 'Monday'),
|
||||
('TUESDAY', 'Tuesday'),
|
||||
('WEDNESDAY', 'Wednesday'),
|
||||
('THURSDAY', 'Thursday'),
|
||||
('FRIDAY', 'Friday'),
|
||||
('SATURDAY', 'Saturday'),
|
||||
('SUNDAY', 'Sunday'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
timezone = models.CharField(max_length=50, default='UTC')
|
||||
|
||||
# Business hours configuration
|
||||
weekday_start = models.TimeField(default=time(9, 0)) # 9:00 AM
|
||||
weekday_end = models.TimeField(default=time(17, 0)) # 5:00 PM
|
||||
weekend_start = models.TimeField(default=time(10, 0)) # 10:00 AM
|
||||
weekend_end = models.TimeField(default=time(16, 0)) # 4:00 PM
|
||||
|
||||
# Specific day overrides
|
||||
day_overrides = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Override hours for specific dates (YYYY-MM-DD format)"
|
||||
)
|
||||
|
||||
# Holiday configuration
|
||||
holiday_calendar = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of holidays (YYYY-MM-DD format) when business hours don't apply"
|
||||
)
|
||||
|
||||
# Status
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False, help_text="Default business hours for the system")
|
||||
|
||||
# Metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['is_active', 'is_default']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.timezone})"
|
||||
|
||||
def is_business_hours(self, check_time: datetime = None) -> bool:
|
||||
"""Check if given time (or now) is within business hours"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
# Convert to business hours timezone
|
||||
if check_time.tzinfo is None:
|
||||
check_time = timezone.make_aware(check_time)
|
||||
|
||||
local_time = check_time.astimezone(ZoneInfo(self.timezone))
|
||||
current_date = local_time.date()
|
||||
current_time = local_time.time()
|
||||
|
||||
# Check for holiday
|
||||
if current_date.strftime('%Y-%m-%d') in self.holiday_calendar:
|
||||
return False
|
||||
|
||||
# Check for day override
|
||||
date_key = current_date.strftime('%Y-%m-%d')
|
||||
if date_key in self.day_overrides:
|
||||
override = self.day_overrides[date_key]
|
||||
if override.get('is_holiday', False):
|
||||
return False
|
||||
start_time = time.fromisoformat(override.get('start_time', '09:00'))
|
||||
end_time = time.fromisoformat(override.get('end_time', '17:00'))
|
||||
return start_time <= current_time <= end_time
|
||||
|
||||
# Regular business hours
|
||||
is_weekend = current_date.weekday() >= 5 # Saturday = 5, Sunday = 6
|
||||
|
||||
if is_weekend:
|
||||
return self.weekend_start <= current_time <= self.weekend_end
|
||||
else:
|
||||
return self.weekday_start <= current_time <= self.weekday_end
|
||||
|
||||
|
||||
class SLADefinition(models.Model):
|
||||
"""Dynamic SLA definitions based on incident type, severity, and business context"""
|
||||
|
||||
SLA_TYPES = [
|
||||
('RESPONSE_TIME', 'Response Time'),
|
||||
('RESOLUTION_TIME', 'Resolution Time'),
|
||||
('ACKNOWLEDGMENT_TIME', 'Acknowledgment Time'),
|
||||
('FIRST_RESPONSE', 'First Response Time'),
|
||||
]
|
||||
|
||||
SEVERITY_CHOICES = [
|
||||
('ALL', 'All Severities'),
|
||||
('LOW', 'Low'),
|
||||
('MEDIUM', 'Medium'),
|
||||
('HIGH', 'High'),
|
||||
('CRITICAL', 'Critical'),
|
||||
('EMERGENCY', 'Emergency'),
|
||||
]
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('ALL', 'All Priorities'),
|
||||
('P1', 'P1 - Critical'),
|
||||
('P2', 'P2 - High'),
|
||||
('P3', 'P3 - Medium'),
|
||||
('P4', 'P4 - Low'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
sla_type = models.CharField(max_length=20, choices=SLA_TYPES)
|
||||
|
||||
# Targeting criteria
|
||||
incident_categories = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident categories this SLA applies to"
|
||||
)
|
||||
incident_severities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident severities this SLA applies to"
|
||||
)
|
||||
incident_priorities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident priorities this SLA applies to"
|
||||
)
|
||||
|
||||
# SLA targets
|
||||
target_duration_minutes = models.PositiveIntegerField(
|
||||
help_text="SLA target in minutes"
|
||||
)
|
||||
business_hours_only = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether SLA only applies during business hours"
|
||||
)
|
||||
business_hours = models.ForeignKey(
|
||||
BusinessHours,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Business hours configuration for this SLA"
|
||||
)
|
||||
|
||||
# Escalation configuration
|
||||
escalation_enabled = models.BooleanField(default=True)
|
||||
escalation_threshold_percent = models.FloatField(
|
||||
default=80.0,
|
||||
validators=[MinValueValidator(0.0), MaxValueValidator(100.0)],
|
||||
help_text="Escalate when X% of SLA time has passed"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['sla_type', 'is_active']),
|
||||
models.Index(fields=['incident_severities']),
|
||||
models.Index(fields=['incident_categories']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.sla_type})"
|
||||
|
||||
def applies_to_incident(self, incident) -> bool:
|
||||
"""Check if this SLA applies to the given incident"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check categories
|
||||
if self.incident_categories and incident.category not in self.incident_categories:
|
||||
return False
|
||||
|
||||
# Check severities
|
||||
if self.incident_severities and incident.severity not in self.incident_severities:
|
||||
return False
|
||||
|
||||
# Check priorities
|
||||
if self.incident_priorities and incident.priority not in self.incident_priorities:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EscalationPolicy(models.Model):
|
||||
"""Escalation policies for incidents and SLAs"""
|
||||
|
||||
ESCALATION_TYPES = [
|
||||
('TIME_BASED', 'Time-based Escalation'),
|
||||
('SEVERITY_BASED', 'Severity-based Escalation'),
|
||||
('RESOURCE_BASED', 'Resource-based Escalation'),
|
||||
('CUSTOM', 'Custom Escalation'),
|
||||
]
|
||||
|
||||
TRIGGER_CONDITIONS = [
|
||||
('SLA_BREACH', 'SLA Breach'),
|
||||
('SLA_THRESHOLD', 'SLA Threshold Reached'),
|
||||
('NO_RESPONSE', 'No Response'),
|
||||
('NO_ACKNOWLEDGMENT', 'No Acknowledgment'),
|
||||
('CUSTOM', 'Custom Condition'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
escalation_type = models.CharField(max_length=20, choices=ESCALATION_TYPES)
|
||||
trigger_condition = models.CharField(max_length=20, choices=TRIGGER_CONDITIONS)
|
||||
|
||||
# Targeting criteria
|
||||
incident_severities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident severities this policy applies to"
|
||||
)
|
||||
incident_categories = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident categories this policy applies to"
|
||||
)
|
||||
|
||||
# Escalation configuration
|
||||
trigger_delay_minutes = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="Delay before escalation triggers (in minutes)"
|
||||
)
|
||||
escalation_steps = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of escalation steps with timing and actions"
|
||||
)
|
||||
|
||||
# Notification configuration
|
||||
notification_channels = models.JSONField(
|
||||
default=list,
|
||||
help_text="Channels to notify during escalation (email, sms, slack, etc.)"
|
||||
)
|
||||
notification_templates = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Templates for different notification channels"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['escalation_type', 'is_active']),
|
||||
models.Index(fields=['trigger_condition']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.escalation_type})"
|
||||
|
||||
def applies_to_incident(self, incident) -> bool:
|
||||
"""Check if this escalation policy applies to the given incident"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check severities
|
||||
if self.incident_severities and incident.severity not in self.incident_severities:
|
||||
return False
|
||||
|
||||
# Check categories
|
||||
if self.incident_categories and incident.category not in self.incident_categories:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class OnCallRotation(models.Model):
|
||||
"""On-call rotation management"""
|
||||
|
||||
ROTATION_TYPES = [
|
||||
('WEEKLY', 'Weekly Rotation'),
|
||||
('DAILY', 'Daily Rotation'),
|
||||
('MONTHLY', 'Monthly Rotation'),
|
||||
('CUSTOM', 'Custom Schedule'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('ACTIVE', 'Active'),
|
||||
('PAUSED', 'Paused'),
|
||||
('INACTIVE', 'Inactive'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
rotation_type = models.CharField(max_length=20, choices=ROTATION_TYPES)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
|
||||
|
||||
# Team configuration
|
||||
team_name = models.CharField(max_length=100)
|
||||
team_description = models.TextField(blank=True, null=True)
|
||||
|
||||
# Schedule configuration
|
||||
schedule_config = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Configuration for the rotation schedule"
|
||||
)
|
||||
timezone = models.CharField(max_length=50, default='UTC')
|
||||
|
||||
# Integration configuration
|
||||
external_system = models.CharField(
|
||||
max_length=50,
|
||||
choices=[
|
||||
('PAGERDUTY', 'PagerDuty'),
|
||||
('OPSGENIE', 'OpsGenie'),
|
||||
('INTERNAL', 'Internal System'),
|
||||
('CUSTOM', 'Custom Integration'),
|
||||
],
|
||||
default='INTERNAL'
|
||||
)
|
||||
external_system_id = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="ID in external system (PagerDuty schedule ID, etc.)"
|
||||
)
|
||||
integration_config = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Configuration for external system integration"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'rotation_type']),
|
||||
models.Index(fields=['external_system']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.rotation_type})"
|
||||
|
||||
def get_current_oncall(self, check_time: datetime = None):
|
||||
"""Get the current on-call person for the given time"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
return self.assignments.filter(
|
||||
start_time__lte=check_time,
|
||||
end_time__gte=check_time,
|
||||
status='ACTIVE'
|
||||
).first()
|
||||
|
||||
|
||||
class OnCallAssignment(models.Model):
|
||||
"""Individual on-call assignments within rotations"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('ACTIVE', 'Active'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
rotation = models.ForeignKey(
|
||||
OnCallRotation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
# Schedule
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SCHEDULED')
|
||||
|
||||
# Handoff information
|
||||
handoff_notes = models.TextField(blank=True, null=True)
|
||||
handed_off_from = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='handed_off_assignments'
|
||||
)
|
||||
handoff_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Performance tracking
|
||||
incidents_handled = models.PositiveIntegerField(default=0)
|
||||
response_time_avg = models.DurationField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['start_time']
|
||||
indexes = [
|
||||
models.Index(fields=['rotation', 'start_time']),
|
||||
models.Index(fields=['user', 'start_time']),
|
||||
models.Index(fields=['status', 'start_time']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} on-call {self.start_time} - {self.end_time}"
|
||||
|
||||
def is_current(self, check_time: datetime = None) -> bool:
|
||||
"""Check if this assignment is currently active"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
return (self.status == 'ACTIVE' and
|
||||
self.start_time <= check_time <= self.end_time)
|
||||
|
||||
|
||||
class SLAInstance(models.Model):
|
||||
"""Instance of SLA tracking for a specific incident"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('ACTIVE', 'Active'),
|
||||
('MET', 'SLA Met'),
|
||||
('BREACHED', 'SLA Breached'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
sla_definition = models.ForeignKey(SLADefinition, on_delete=models.CASCADE)
|
||||
incident = models.ForeignKey(
|
||||
'incident_intelligence.Incident',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='sla_instances'
|
||||
)
|
||||
|
||||
# SLA tracking
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
|
||||
target_time = models.DateTimeField(help_text="When the SLA should be met")
|
||||
|
||||
# Timing
|
||||
started_at = models.DateTimeField(auto_now_add=True)
|
||||
met_at = models.DateTimeField(null=True, blank=True)
|
||||
breached_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Escalation tracking
|
||||
escalation_policy = models.ForeignKey(
|
||||
EscalationPolicy,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
escalation_triggered = models.BooleanField(default=False)
|
||||
escalation_triggered_at = models.DateTimeField(null=True, blank=True)
|
||||
escalation_level = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Performance metrics
|
||||
response_time = models.DurationField(null=True, blank=True)
|
||||
resolution_time = models.DurationField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['incident', 'status']),
|
||||
models.Index(fields=['sla_definition', 'status']),
|
||||
models.Index(fields=['target_time', 'status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"SLA {self.sla_definition.name} for {self.incident.title}"
|
||||
|
||||
@property
|
||||
def is_breached(self) -> bool:
|
||||
"""Check if SLA is breached"""
|
||||
if self.status == 'BREACHED':
|
||||
return True
|
||||
|
||||
if self.status == 'ACTIVE':
|
||||
return timezone.now() > self.target_time
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> timedelta:
|
||||
"""Get time remaining until SLA breach"""
|
||||
if self.status != 'ACTIVE':
|
||||
return timedelta(0)
|
||||
|
||||
remaining = self.target_time - timezone.now()
|
||||
return remaining if remaining > timedelta(0) else timedelta(0)
|
||||
|
||||
@property
|
||||
def breach_time(self) -> timedelta:
|
||||
"""Get time since SLA breach"""
|
||||
if not self.is_breached:
|
||||
return timedelta(0)
|
||||
|
||||
return timezone.now() - self.target_time
|
||||
|
||||
|
||||
class EscalationInstance(models.Model):
|
||||
"""Instance of escalation for a specific incident"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('TRIGGERED', 'Triggered'),
|
||||
('ACKNOWLEDGED', 'Acknowledged'),
|
||||
('RESOLVED', 'Resolved'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
escalation_policy = models.ForeignKey(EscalationPolicy, on_delete=models.CASCADE)
|
||||
incident = models.ForeignKey(
|
||||
'incident_intelligence.Incident',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='escalation_instances'
|
||||
)
|
||||
sla_instance = models.ForeignKey(
|
||||
SLAInstance,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='escalation_instances'
|
||||
)
|
||||
|
||||
# Escalation details
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
||||
escalation_level = models.PositiveIntegerField(default=1)
|
||||
current_step = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Timing
|
||||
triggered_at = models.DateTimeField(null=True, blank=True)
|
||||
acknowledged_at = models.DateTimeField(null=True, blank=True)
|
||||
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Actions taken
|
||||
notifications_sent = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of notifications sent during escalation"
|
||||
)
|
||||
actions_taken = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of actions taken during escalation"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['incident', 'status']),
|
||||
models.Index(fields=['escalation_policy', 'status']),
|
||||
models.Index(fields=['triggered_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Escalation {self.escalation_policy.name} for {self.incident.title}"
|
||||
|
||||
|
||||
class NotificationTemplate(models.Model):
|
||||
"""Templates for escalation and on-call notifications"""
|
||||
|
||||
TEMPLATE_TYPES = [
|
||||
('ESCALATION', 'Escalation Notification'),
|
||||
('ONCALL_HANDOFF', 'On-Call Handoff'),
|
||||
('SLA_BREACH', 'SLA Breach Alert'),
|
||||
('SLA_WARNING', 'SLA Warning'),
|
||||
('CUSTOM', 'Custom Notification'),
|
||||
]
|
||||
|
||||
CHANNEL_TYPES = [
|
||||
('EMAIL', 'Email'),
|
||||
('SMS', 'SMS'),
|
||||
('SLACK', 'Slack'),
|
||||
('TEAMS', 'Microsoft Teams'),
|
||||
('WEBHOOK', 'Webhook'),
|
||||
('CUSTOM', 'Custom Channel'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
template_type = models.CharField(max_length=20, choices=TEMPLATE_TYPES)
|
||||
channel_type = models.CharField(max_length=20, choices=CHANNEL_TYPES)
|
||||
|
||||
# Template content
|
||||
subject_template = models.CharField(
|
||||
max_length=500,
|
||||
help_text="Subject template with variables"
|
||||
)
|
||||
body_template = models.TextField(
|
||||
help_text="Body template with variables"
|
||||
)
|
||||
variables = models.JSONField(
|
||||
default=list,
|
||||
help_text="Available variables for this template"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['template_type', 'channel_type', 'name']
|
||||
unique_together = ['template_type', 'channel_type', 'name']
|
||||
indexes = [
|
||||
models.Index(fields=['template_type', 'channel_type']),
|
||||
models.Index(fields=['is_active']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.template_type} - {self.channel_type})"
|
||||
1
ETB-API/sla_oncall/serializers/__init__.py
Normal file
1
ETB-API/sla_oncall/serializers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Serializers for SLA & On-Call Management
|
||||
Binary file not shown.
BIN
ETB-API/sla_oncall/serializers/__pycache__/sla.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/serializers/__pycache__/sla.cpython-312.pyc
Normal file
Binary file not shown.
281
ETB-API/sla_oncall/serializers/sla.py
Normal file
281
ETB-API/sla_oncall/serializers/sla.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Serializers for SLA & On-Call Management
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from sla_oncall.models import (
|
||||
BusinessHours,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
SLAInstance,
|
||||
EscalationInstance,
|
||||
NotificationTemplate,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BusinessHoursSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for BusinessHours model"""
|
||||
|
||||
class Meta:
|
||||
model = BusinessHours
|
||||
fields = [
|
||||
'id', 'name', 'description', 'timezone',
|
||||
'weekday_start', 'weekday_end', 'weekend_start', 'weekend_end',
|
||||
'day_overrides', 'holiday_calendar',
|
||||
'is_active', 'is_default',
|
||||
'created_by', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate business hours configuration"""
|
||||
if data.get('weekday_start') >= data.get('weekday_end'):
|
||||
raise serializers.ValidationError(
|
||||
"Weekday start time must be before weekday end time"
|
||||
)
|
||||
|
||||
if data.get('weekend_start') >= data.get('weekend_end'):
|
||||
raise serializers.ValidationError(
|
||||
"Weekend start time must be before weekend end time"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class SLADefinitionSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for SLADefinition model"""
|
||||
|
||||
business_hours_name = serializers.CharField(
|
||||
source='business_hours.name',
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SLADefinition
|
||||
fields = [
|
||||
'id', 'name', 'description', 'sla_type',
|
||||
'incident_categories', 'incident_severities', 'incident_priorities',
|
||||
'target_duration_minutes', 'business_hours_only', 'business_hours',
|
||||
'business_hours_name',
|
||||
'escalation_enabled', 'escalation_threshold_percent',
|
||||
'is_active', 'is_default',
|
||||
'created_by', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def validate_escalation_threshold_percent(self, value):
|
||||
"""Validate escalation threshold percentage"""
|
||||
if not (0 <= value <= 100):
|
||||
raise serializers.ValidationError(
|
||||
"Escalation threshold must be between 0 and 100 percent"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class EscalationPolicySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for EscalationPolicy model"""
|
||||
|
||||
class Meta:
|
||||
model = EscalationPolicy
|
||||
fields = [
|
||||
'id', 'name', 'description', 'escalation_type', 'trigger_condition',
|
||||
'incident_severities', 'incident_categories',
|
||||
'trigger_delay_minutes', 'escalation_steps',
|
||||
'notification_channels', 'notification_templates',
|
||||
'is_active', 'is_default',
|
||||
'created_by', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class OnCallRotationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for OnCallRotation model"""
|
||||
|
||||
current_oncall = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = OnCallRotation
|
||||
fields = [
|
||||
'id', 'name', 'description', 'rotation_type', 'status',
|
||||
'team_name', 'team_description',
|
||||
'schedule_config', 'timezone',
|
||||
'external_system', 'external_system_id', 'integration_config',
|
||||
'current_oncall',
|
||||
'created_by', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'current_oncall']
|
||||
|
||||
def get_current_oncall(self, obj):
|
||||
"""Get current on-call person"""
|
||||
current_assignment = obj.get_current_oncall()
|
||||
if current_assignment:
|
||||
return {
|
||||
'user_id': current_assignment.user.id,
|
||||
'username': current_assignment.user.username,
|
||||
'start_time': current_assignment.start_time,
|
||||
'end_time': current_assignment.end_time
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class OnCallAssignmentSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for OnCallAssignment model"""
|
||||
|
||||
user_name = serializers.CharField(source='user.username', read_only=True)
|
||||
user_email = serializers.CharField(source='user.email', read_only=True)
|
||||
rotation_name = serializers.CharField(source='rotation.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OnCallAssignment
|
||||
fields = [
|
||||
'id', 'rotation', 'rotation_name', 'user', 'user_name', 'user_email',
|
||||
'start_time', 'end_time', 'status',
|
||||
'handoff_notes', 'handed_off_from', 'handoff_time',
|
||||
'incidents_handled', 'response_time_avg',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'updated_at', 'user_name', 'user_email', 'rotation_name'
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate assignment times"""
|
||||
if data.get('start_time') >= data.get('end_time'):
|
||||
raise serializers.ValidationError(
|
||||
"Start time must be before end time"
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class SLAInstanceSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for SLAInstance model"""
|
||||
|
||||
incident_title = serializers.CharField(source='incident.title', read_only=True)
|
||||
sla_definition_name = serializers.CharField(source='sla_definition.name', read_only=True)
|
||||
is_breached = serializers.BooleanField(read_only=True)
|
||||
time_remaining = serializers.DurationField(read_only=True)
|
||||
breach_time = serializers.DurationField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SLAInstance
|
||||
fields = [
|
||||
'id', 'sla_definition', 'sla_definition_name', 'incident', 'incident_title',
|
||||
'status', 'target_time',
|
||||
'started_at', 'met_at', 'breached_at',
|
||||
'escalation_policy', 'escalation_triggered', 'escalation_triggered_at',
|
||||
'escalation_level', 'response_time', 'resolution_time',
|
||||
'is_breached', 'time_remaining', 'breach_time',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'updated_at', 'started_at',
|
||||
'is_breached', 'time_remaining', 'breach_time',
|
||||
'incident_title', 'sla_definition_name'
|
||||
]
|
||||
|
||||
|
||||
class EscalationInstanceSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for EscalationInstance model"""
|
||||
|
||||
incident_title = serializers.CharField(source='incident.title', read_only=True)
|
||||
escalation_policy_name = serializers.CharField(source='escalation_policy.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = EscalationInstance
|
||||
fields = [
|
||||
'id', 'escalation_policy', 'escalation_policy_name',
|
||||
'incident', 'incident_title', 'sla_instance',
|
||||
'status', 'escalation_level', 'current_step',
|
||||
'triggered_at', 'acknowledged_at', 'resolved_at',
|
||||
'notifications_sent', 'actions_taken',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'updated_at',
|
||||
'incident_title', 'escalation_policy_name'
|
||||
]
|
||||
|
||||
|
||||
class NotificationTemplateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for NotificationTemplate model"""
|
||||
|
||||
class Meta:
|
||||
model = NotificationTemplate
|
||||
fields = [
|
||||
'id', 'name', 'template_type', 'channel_type',
|
||||
'subject_template', 'body_template', 'variables',
|
||||
'is_active', 'is_default',
|
||||
'created_by', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class SLAInstanceCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating SLA instances"""
|
||||
|
||||
class Meta:
|
||||
model = SLAInstance
|
||||
fields = [
|
||||
'sla_definition', 'incident', 'escalation_policy'
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create SLA instance with calculated target time"""
|
||||
sla_instance = super().create(validated_data)
|
||||
|
||||
# Calculate target time based on SLA definition
|
||||
sla_definition = sla_instance.sla_definition
|
||||
incident = sla_instance.incident
|
||||
|
||||
if sla_definition.business_hours_only and sla_definition.business_hours:
|
||||
# Calculate target time considering business hours
|
||||
target_time = sla_definition.business_hours.calculate_target_time(
|
||||
incident.created_at,
|
||||
sla_definition.target_duration_minutes
|
||||
)
|
||||
else:
|
||||
# Simple time calculation
|
||||
target_time = incident.created_at + timedelta(
|
||||
minutes=sla_definition.target_duration_minutes
|
||||
)
|
||||
|
||||
sla_instance.target_time = target_time
|
||||
sla_instance.save()
|
||||
|
||||
return sla_instance
|
||||
|
||||
|
||||
class OnCallAssignmentCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating on-call assignments"""
|
||||
|
||||
class Meta:
|
||||
model = OnCallAssignment
|
||||
fields = [
|
||||
'rotation', 'user', 'start_time', 'end_time', 'handoff_notes'
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create on-call assignment with validation"""
|
||||
assignment = super().create(validated_data)
|
||||
|
||||
# Check for overlapping assignments
|
||||
overlapping = OnCallAssignment.objects.filter(
|
||||
rotation=assignment.rotation,
|
||||
user=assignment.user,
|
||||
status__in=['SCHEDULED', 'ACTIVE'],
|
||||
start_time__lt=assignment.end_time,
|
||||
end_time__gt=assignment.start_time
|
||||
).exclude(id=assignment.id)
|
||||
|
||||
if overlapping.exists():
|
||||
raise serializers.ValidationError(
|
||||
"This assignment overlaps with existing assignments for the same user"
|
||||
)
|
||||
|
||||
return assignment
|
||||
196
ETB-API/sla_oncall/signals.py
Normal file
196
ETB-API/sla_oncall/signals.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Signals for SLA & On-Call Management
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from sla_oncall.models import (
|
||||
SLAInstance,
|
||||
EscalationInstance,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
)
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
|
||||
@receiver(post_save, sender=Incident)
|
||||
def create_sla_instances(sender, instance, created, **kwargs):
|
||||
"""Automatically create SLA instances when incidents are created"""
|
||||
if created:
|
||||
# Find applicable SLA definitions
|
||||
applicable_slas = SLADefinition.objects.filter(
|
||||
is_active=True,
|
||||
incident_categories__contains=[instance.category] if instance.category else [],
|
||||
incident_severities__contains=[instance.severity] if instance.severity else [],
|
||||
incident_priorities__contains=[instance.priority] if instance.priority else [],
|
||||
)
|
||||
|
||||
# Also check for SLAs that apply to all categories/severities/priorities
|
||||
all_applicable_slas = SLADefinition.objects.filter(
|
||||
is_active=True
|
||||
).filter(
|
||||
models.Q(incident_categories__isnull=True) | models.Q(incident_categories=[]) |
|
||||
models.Q(incident_severities__isnull=True) | models.Q(incident_severities=[]) |
|
||||
models.Q(incident_priorities__isnull=True) | models.Q(incident_priorities=[])
|
||||
)
|
||||
|
||||
# Combine and deduplicate
|
||||
all_slas = (applicable_slas | all_applicable_slas).distinct()
|
||||
|
||||
for sla_definition in all_slas:
|
||||
if sla_definition.applies_to_incident(instance):
|
||||
# Find applicable escalation policy
|
||||
escalation_policy = None
|
||||
if sla_definition.escalation_enabled:
|
||||
escalation_policy = EscalationPolicy.objects.filter(
|
||||
is_active=True,
|
||||
incident_severities__contains=[instance.severity] if instance.severity else [],
|
||||
incident_categories__contains=[instance.category] if instance.category else [],
|
||||
).first()
|
||||
|
||||
# Calculate target time
|
||||
if sla_definition.business_hours_only and sla_definition.business_hours:
|
||||
target_time = sla_definition.business_hours.calculate_target_time(
|
||||
instance.created_at,
|
||||
sla_definition.target_duration_minutes
|
||||
)
|
||||
else:
|
||||
target_time = instance.created_at + timedelta(
|
||||
minutes=sla_definition.target_duration_minutes
|
||||
)
|
||||
|
||||
# Create SLA instance
|
||||
SLAInstance.objects.create(
|
||||
sla_definition=sla_definition,
|
||||
incident=instance,
|
||||
target_time=target_time,
|
||||
escalation_policy=escalation_policy
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=SLAInstance)
|
||||
def check_sla_breach(sender, instance, **kwargs):
|
||||
"""Check for SLA breach and trigger escalation if needed"""
|
||||
if instance.pk: # Only for existing instances
|
||||
try:
|
||||
old_instance = SLAInstance.objects.get(pk=instance.pk)
|
||||
|
||||
# Check if SLA just became breached
|
||||
if (old_instance.status == 'ACTIVE' and
|
||||
instance.status == 'BREACHED' and
|
||||
not instance.breached_at):
|
||||
instance.breached_at = timezone.now()
|
||||
|
||||
# Trigger escalation if policy exists and not already triggered
|
||||
if (instance.escalation_policy and
|
||||
not instance.escalation_triggered):
|
||||
trigger_escalation(instance)
|
||||
|
||||
except SLAInstance.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=SLAInstance)
|
||||
def schedule_escalation_check(sender, instance, created, **kwargs):
|
||||
"""Schedule escalation check for active SLA instances"""
|
||||
if instance.status == 'ACTIVE' and instance.escalation_policy:
|
||||
# Check if escalation should be triggered based on threshold
|
||||
threshold_time = instance.started_at + timedelta(
|
||||
minutes=instance.sla_definition.target_duration_minutes *
|
||||
(instance.escalation_policy.escalation_threshold_percent / 100)
|
||||
)
|
||||
|
||||
if (timezone.now() >= threshold_time and
|
||||
not instance.escalation_triggered):
|
||||
trigger_escalation(instance)
|
||||
|
||||
|
||||
def trigger_escalation(sla_instance):
|
||||
"""Trigger escalation for an SLA instance"""
|
||||
escalation_policy = sla_instance.escalation_policy
|
||||
|
||||
# Create escalation instance
|
||||
escalation_instance = EscalationInstance.objects.create(
|
||||
escalation_policy=escalation_policy,
|
||||
incident=sla_instance.incident,
|
||||
sla_instance=sla_instance,
|
||||
status='TRIGGERED',
|
||||
triggered_at=timezone.now(),
|
||||
escalation_level=1
|
||||
)
|
||||
|
||||
# Update SLA instance
|
||||
sla_instance.escalation_triggered = True
|
||||
sla_instance.escalation_triggered_at = timezone.now()
|
||||
sla_instance.escalation_level = 1
|
||||
sla_instance.save()
|
||||
|
||||
# Send notifications and perform escalation actions
|
||||
perform_escalation_actions(escalation_instance)
|
||||
|
||||
|
||||
def perform_escalation_actions(escalation_instance):
|
||||
"""Perform escalation actions based on policy"""
|
||||
policy = escalation_instance.escalation_policy
|
||||
incident = escalation_instance.incident
|
||||
|
||||
# Get current on-call person if applicable
|
||||
current_oncall = get_current_oncall_for_incident(incident)
|
||||
|
||||
# Send notifications based on policy configuration
|
||||
notifications_sent = []
|
||||
|
||||
for channel in policy.notification_channels:
|
||||
notification_data = {
|
||||
'channel': channel,
|
||||
'sent_at': timezone.now().isoformat(),
|
||||
'recipients': get_notification_recipients(channel, current_oncall),
|
||||
'template': policy.notification_templates.get(channel, {})
|
||||
}
|
||||
notifications_sent.append(notification_data)
|
||||
|
||||
escalation_instance.notifications_sent = notifications_sent
|
||||
escalation_instance.save()
|
||||
|
||||
# Log escalation action
|
||||
actions_taken = [{
|
||||
'action': 'escalation_triggered',
|
||||
'timestamp': timezone.now().isoformat(),
|
||||
'level': escalation_instance.escalation_level,
|
||||
'policy': policy.name
|
||||
}]
|
||||
|
||||
escalation_instance.actions_taken = actions_taken
|
||||
escalation_instance.save()
|
||||
|
||||
|
||||
def get_current_oncall_for_incident(incident):
|
||||
"""Get current on-call person for an incident"""
|
||||
# This is a simplified implementation
|
||||
# In a real system, you might have more complex logic to determine
|
||||
# which on-call rotation applies to which incidents
|
||||
from sla_oncall.models import OnCallAssignment
|
||||
|
||||
current_assignment = OnCallAssignment.objects.filter(
|
||||
status='ACTIVE',
|
||||
start_time__lte=timezone.now(),
|
||||
end_time__gte=timezone.now()
|
||||
).first()
|
||||
|
||||
return current_assignment.user if current_assignment else None
|
||||
|
||||
|
||||
def get_notification_recipients(channel, current_oncall):
|
||||
"""Get notification recipients for a channel"""
|
||||
recipients = []
|
||||
|
||||
if current_oncall:
|
||||
if channel == 'email' and current_oncall.email:
|
||||
recipients.append(current_oncall.email)
|
||||
elif channel == 'sms' and hasattr(current_oncall, 'phone_number'):
|
||||
recipients.append(getattr(current_oncall, 'phone_number', ''))
|
||||
|
||||
return recipients
|
||||
151
ETB-API/sla_oncall/tests.py
Normal file
151
ETB-API/sla_oncall/tests.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Tests for SLA & On-Call Management
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta, time
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from sla_oncall.models import (
|
||||
BusinessHours,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
SLAInstance,
|
||||
EscalationInstance,
|
||||
NotificationTemplate,
|
||||
)
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BusinessHoursModelTest(TestCase):
|
||||
"""Test BusinessHours model functionality"""
|
||||
|
||||
def setUp(self):
|
||||
self.business_hours = BusinessHours.objects.create(
|
||||
name='Test Business Hours',
|
||||
description='Test business hours',
|
||||
timezone='UTC',
|
||||
weekday_start=time(9, 0),
|
||||
weekday_end=time(17, 0),
|
||||
weekend_start=time(10, 0),
|
||||
weekend_end=time(16, 0),
|
||||
)
|
||||
|
||||
def test_business_hours_creation(self):
|
||||
"""Test business hours creation"""
|
||||
self.assertEqual(self.business_hours.name, 'Test Business Hours')
|
||||
self.assertEqual(self.business_hours.weekday_start, time(9, 0))
|
||||
self.assertTrue(self.business_hours.is_active)
|
||||
|
||||
def test_is_business_hours_weekday(self):
|
||||
"""Test business hours check for weekday"""
|
||||
# Monday 10 AM UTC
|
||||
test_time = timezone.make_aware(datetime(2024, 1, 8, 10, 0))
|
||||
self.assertTrue(self.business_hours.is_business_hours(test_time))
|
||||
|
||||
# Monday 6 AM UTC (before business hours)
|
||||
test_time = timezone.make_aware(datetime(2024, 1, 8, 6, 0))
|
||||
self.assertFalse(self.business_hours.is_business_hours(test_time))
|
||||
|
||||
|
||||
class SLADefinitionModelTest(TestCase):
|
||||
"""Test SLADefinition model functionality"""
|
||||
|
||||
def setUp(self):
|
||||
self.business_hours = BusinessHours.objects.create(
|
||||
name='Test Business Hours',
|
||||
description='Test business hours',
|
||||
timezone='UTC',
|
||||
)
|
||||
|
||||
self.sla_definition = SLADefinition.objects.create(
|
||||
name='Test SLA',
|
||||
description='Test SLA definition',
|
||||
sla_type='RESPONSE_TIME',
|
||||
incident_severities=['HIGH', 'CRITICAL'],
|
||||
incident_categories=['SYSTEM'],
|
||||
target_duration_minutes=60,
|
||||
business_hours=self.business_hours,
|
||||
)
|
||||
|
||||
def test_sla_definition_creation(self):
|
||||
"""Test SLA definition creation"""
|
||||
self.assertEqual(self.sla_definition.name, 'Test SLA')
|
||||
self.assertEqual(self.sla_definition.target_duration_minutes, 60)
|
||||
self.assertEqual(self.sla_definition.escalation_threshold_percent, 80.0)
|
||||
|
||||
|
||||
class OnCallRotationModelTest(TestCase):
|
||||
"""Test OnCallRotation model functionality"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
email='test@example.com',
|
||||
password='testpass123'
|
||||
)
|
||||
|
||||
self.rotation = OnCallRotation.objects.create(
|
||||
name='Test Rotation',
|
||||
description='Test rotation',
|
||||
rotation_type='WEEKLY',
|
||||
team_name='Test Team',
|
||||
)
|
||||
|
||||
def test_rotation_creation(self):
|
||||
"""Test on-call rotation creation"""
|
||||
self.assertEqual(self.rotation.name, 'Test Rotation')
|
||||
self.assertEqual(self.rotation.rotation_type, 'WEEKLY')
|
||||
self.assertEqual(self.rotation.status, 'ACTIVE')
|
||||
|
||||
|
||||
class SLAInstanceModelTest(TestCase):
|
||||
"""Test SLAInstance model functionality"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
email='test@example.com',
|
||||
password='testpass123'
|
||||
)
|
||||
|
||||
self.incident = Incident.objects.create(
|
||||
title='Test Incident',
|
||||
description='Test incident description',
|
||||
severity='HIGH',
|
||||
category='SYSTEM',
|
||||
reporter=self.user,
|
||||
)
|
||||
|
||||
self.sla_definition = SLADefinition.objects.create(
|
||||
name='Test SLA',
|
||||
description='Test SLA definition',
|
||||
sla_type='RESPONSE_TIME',
|
||||
target_duration_minutes=60,
|
||||
)
|
||||
|
||||
self.sla_instance = SLAInstance.objects.create(
|
||||
sla_definition=self.sla_definition,
|
||||
incident=self.incident,
|
||||
target_time=timezone.now() + timedelta(minutes=60),
|
||||
)
|
||||
|
||||
def test_sla_instance_creation(self):
|
||||
"""Test SLA instance creation"""
|
||||
self.assertEqual(self.sla_instance.incident, self.incident)
|
||||
self.assertEqual(self.sla_instance.sla_definition, self.sla_definition)
|
||||
self.assertEqual(self.sla_instance.status, 'ACTIVE')
|
||||
|
||||
def test_is_breached_property(self):
|
||||
"""Test SLA breach detection"""
|
||||
# Active SLA with future target time
|
||||
self.assertFalse(self.sla_instance.is_breached)
|
||||
|
||||
# Active SLA with past target time
|
||||
self.sla_instance.target_time = timezone.now() - timedelta(minutes=1)
|
||||
self.assertTrue(self.sla_instance.is_breached)
|
||||
33
ETB-API/sla_oncall/urls.py
Normal file
33
ETB-API/sla_oncall/urls.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
URL configuration for SLA & On-Call Management API
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from sla_oncall.views.sla import (
|
||||
BusinessHoursViewSet,
|
||||
SLADefinitionViewSet,
|
||||
EscalationPolicyViewSet,
|
||||
OnCallRotationViewSet,
|
||||
OnCallAssignmentViewSet,
|
||||
SLAInstanceViewSet,
|
||||
EscalationInstanceViewSet,
|
||||
NotificationTemplateViewSet,
|
||||
)
|
||||
|
||||
# Create router and register viewsets
|
||||
router = DefaultRouter()
|
||||
router.register(r'business-hours', BusinessHoursViewSet)
|
||||
router.register(r'sla-definitions', SLADefinitionViewSet)
|
||||
router.register(r'escalation-policies', EscalationPolicyViewSet)
|
||||
router.register(r'oncall-rotations', OnCallRotationViewSet)
|
||||
router.register(r'oncall-assignments', OnCallAssignmentViewSet)
|
||||
router.register(r'sla-instances', SLAInstanceViewSet)
|
||||
router.register(r'escalation-instances', EscalationInstanceViewSet)
|
||||
router.register(r'notification-templates', NotificationTemplateViewSet)
|
||||
|
||||
app_name = 'sla_oncall'
|
||||
|
||||
urlpatterns = [
|
||||
path('api/v1/', include(router.urls)),
|
||||
]
|
||||
3
ETB-API/sla_oncall/views.py
Normal file
3
ETB-API/sla_oncall/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
1
ETB-API/sla_oncall/views/__init__.py
Normal file
1
ETB-API/sla_oncall/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Views for SLA & On-Call Management
|
||||
BIN
ETB-API/sla_oncall/views/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/views/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ETB-API/sla_oncall/views/__pycache__/sla.cpython-312.pyc
Normal file
BIN
ETB-API/sla_oncall/views/__pycache__/sla.cpython-312.pyc
Normal file
Binary file not shown.
354
ETB-API/sla_oncall/views/sla.py
Normal file
354
ETB-API/sla_oncall/views/sla.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
Views for SLA & On-Call Management API
|
||||
"""
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sla_oncall.models import (
|
||||
BusinessHours,
|
||||
SLADefinition,
|
||||
EscalationPolicy,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
SLAInstance,
|
||||
EscalationInstance,
|
||||
NotificationTemplate,
|
||||
)
|
||||
from sla_oncall.serializers.sla import (
|
||||
BusinessHoursSerializer,
|
||||
SLADefinitionSerializer,
|
||||
EscalationPolicySerializer,
|
||||
OnCallRotationSerializer,
|
||||
OnCallAssignmentSerializer,
|
||||
SLAInstanceSerializer,
|
||||
EscalationInstanceSerializer,
|
||||
NotificationTemplateSerializer,
|
||||
SLAInstanceCreateSerializer,
|
||||
OnCallAssignmentCreateSerializer,
|
||||
)
|
||||
|
||||
|
||||
class BusinessHoursViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for BusinessHours management"""
|
||||
|
||||
queryset = BusinessHours.objects.all()
|
||||
serializer_class = BusinessHoursSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['is_active', 'is_default', 'timezone']
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
ordering = ['name']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def test_business_hours(self, request, pk=None):
|
||||
"""Test if a given time is within business hours"""
|
||||
business_hours = self.get_object()
|
||||
test_time = request.data.get('test_time')
|
||||
|
||||
if test_time:
|
||||
try:
|
||||
test_datetime = datetime.fromisoformat(test_time.replace('Z', '+00:00'))
|
||||
is_business_hours = business_hours.is_business_hours(test_datetime)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{'error': 'Invalid datetime format. Use ISO format.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
else:
|
||||
is_business_hours = business_hours.is_business_hours()
|
||||
|
||||
return Response({
|
||||
'is_business_hours': is_business_hours,
|
||||
'test_time': test_time or timezone.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
class SLADefinitionViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for SLA Definition management"""
|
||||
|
||||
queryset = SLADefinition.objects.all()
|
||||
serializer_class = SLADefinitionSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['sla_type', 'is_active', 'is_default', 'business_hours_only']
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'created_at', 'target_duration_minutes']
|
||||
ordering = ['name']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def test_applicability(self, request, pk=None):
|
||||
"""Test if SLA definition applies to a given incident"""
|
||||
sla_definition = self.get_object()
|
||||
incident_data = request.data
|
||||
|
||||
# Create a mock incident object for testing
|
||||
class MockIncident:
|
||||
def __init__(self, data):
|
||||
self.category = data.get('category')
|
||||
self.severity = data.get('severity')
|
||||
self.priority = data.get('priority')
|
||||
|
||||
mock_incident = MockIncident(incident_data)
|
||||
applies = sla_definition.applies_to_incident(mock_incident)
|
||||
|
||||
return Response({
|
||||
'applies': applies,
|
||||
'incident_data': incident_data
|
||||
})
|
||||
|
||||
|
||||
class EscalationPolicyViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for Escalation Policy management"""
|
||||
|
||||
queryset = EscalationPolicy.objects.all()
|
||||
serializer_class = EscalationPolicySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['escalation_type', 'trigger_condition', 'is_active']
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class OnCallRotationViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for On-Call Rotation management"""
|
||||
|
||||
queryset = OnCallRotation.objects.all()
|
||||
serializer_class = OnCallRotationSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['rotation_type', 'status', 'external_system']
|
||||
search_fields = ['name', 'team_name', 'description']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
ordering = ['name']
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def current_oncall(self, request, pk=None):
|
||||
"""Get current on-call person for this rotation"""
|
||||
rotation = self.get_object()
|
||||
current_assignment = rotation.get_current_oncall()
|
||||
|
||||
if current_assignment:
|
||||
serializer = OnCallAssignmentSerializer(current_assignment)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response({'message': 'No one is currently on-call'})
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def upcoming_assignments(self, request, pk=None):
|
||||
"""Get upcoming on-call assignments"""
|
||||
rotation = self.get_object()
|
||||
days_ahead = int(request.query_params.get('days', 30))
|
||||
|
||||
future_time = timezone.now() + timedelta(days=days_ahead)
|
||||
upcoming = rotation.assignments.filter(
|
||||
start_time__lte=future_time,
|
||||
start_time__gte=timezone.now()
|
||||
).order_by('start_time')
|
||||
|
||||
serializer = OnCallAssignmentSerializer(upcoming, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class OnCallAssignmentViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for On-Call Assignment management"""
|
||||
|
||||
queryset = OnCallAssignment.objects.all()
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['rotation', 'user', 'status']
|
||||
search_fields = ['user__username', 'user__email', 'rotation__name']
|
||||
ordering_fields = ['start_time', 'end_time', 'created_at']
|
||||
ordering = ['-start_time']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action"""
|
||||
if self.action == 'create':
|
||||
return OnCallAssignmentCreateSerializer
|
||||
return OnCallAssignmentSerializer
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def handoff(self, request, pk=None):
|
||||
"""Perform on-call handoff"""
|
||||
assignment = self.get_object()
|
||||
handoff_notes = request.data.get('handoff_notes', '')
|
||||
handed_off_from = request.user
|
||||
|
||||
assignment.handoff_notes = handoff_notes
|
||||
assignment.handed_off_from = handed_off_from
|
||||
assignment.handoff_time = timezone.now()
|
||||
assignment.save()
|
||||
|
||||
return Response({'message': 'Handoff completed successfully'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def activate(self, request, pk=None):
|
||||
"""Activate on-call assignment"""
|
||||
assignment = self.get_object()
|
||||
|
||||
if assignment.status != 'SCHEDULED':
|
||||
return Response(
|
||||
{'error': 'Only scheduled assignments can be activated'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
assignment.status = 'ACTIVE'
|
||||
assignment.save()
|
||||
|
||||
return Response({'message': 'Assignment activated'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def complete(self, request, pk=None):
|
||||
"""Complete on-call assignment"""
|
||||
assignment = self.get_object()
|
||||
|
||||
if assignment.status != 'ACTIVE':
|
||||
return Response(
|
||||
{'error': 'Only active assignments can be completed'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
assignment.status = 'COMPLETED'
|
||||
assignment.save()
|
||||
|
||||
return Response({'message': 'Assignment completed'})
|
||||
|
||||
|
||||
class SLAInstanceViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for SLA Instance management"""
|
||||
|
||||
queryset = SLAInstance.objects.all()
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['status', 'escalation_triggered', 'sla_definition']
|
||||
search_fields = ['incident__title', 'sla_definition__name']
|
||||
ordering_fields = ['created_at', 'target_time', 'started_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action"""
|
||||
if self.action == 'create':
|
||||
return SLAInstanceCreateSerializer
|
||||
return SLAInstanceSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def breached(self, request):
|
||||
"""Get all breached SLA instances"""
|
||||
breached_slas = self.queryset.filter(
|
||||
status='BREACHED'
|
||||
).order_by('-breached_at')
|
||||
|
||||
serializer = self.get_serializer(breached_slas, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def at_risk(self, request):
|
||||
"""Get SLA instances at risk of breaching (within 15 minutes)"""
|
||||
warning_time = timezone.now() + timedelta(minutes=15)
|
||||
at_risk_slas = self.queryset.filter(
|
||||
status='ACTIVE',
|
||||
target_time__lte=warning_time
|
||||
).order_by('target_time')
|
||||
|
||||
serializer = self.get_serializer(at_risk_slas, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def mark_met(self, request, pk=None):
|
||||
"""Mark SLA as met"""
|
||||
sla_instance = self.get_object()
|
||||
|
||||
if sla_instance.status != 'ACTIVE':
|
||||
return Response(
|
||||
{'error': 'Only active SLA instances can be marked as met'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
sla_instance.status = 'MET'
|
||||
sla_instance.met_at = timezone.now()
|
||||
sla_instance.save()
|
||||
|
||||
return Response({'message': 'SLA marked as met'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def mark_breached(self, request, pk=None):
|
||||
"""Mark SLA as breached"""
|
||||
sla_instance = self.get_object()
|
||||
|
||||
if sla_instance.status != 'ACTIVE':
|
||||
return Response(
|
||||
{'error': 'Only active SLA instances can be marked as breached'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
sla_instance.status = 'BREACHED'
|
||||
sla_instance.breached_at = timezone.now()
|
||||
sla_instance.save()
|
||||
|
||||
return Response({'message': 'SLA marked as breached'})
|
||||
|
||||
|
||||
class EscalationInstanceViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for Escalation Instance management"""
|
||||
|
||||
queryset = EscalationInstance.objects.all()
|
||||
serializer_class = EscalationInstanceSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['status', 'escalation_level', 'escalation_policy']
|
||||
search_fields = ['incident__title', 'escalation_policy__name']
|
||||
ordering_fields = ['created_at', 'triggered_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def acknowledge(self, request, pk=None):
|
||||
"""Acknowledge escalation"""
|
||||
escalation = self.get_object()
|
||||
|
||||
if escalation.status not in ['TRIGGERED', 'PENDING']:
|
||||
return Response(
|
||||
{'error': 'Only pending or triggered escalations can be acknowledged'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
escalation.status = 'ACKNOWLEDGED'
|
||||
escalation.acknowledged_at = timezone.now()
|
||||
escalation.save()
|
||||
|
||||
return Response({'message': 'Escalation acknowledged'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def resolve(self, request, pk=None):
|
||||
"""Resolve escalation"""
|
||||
escalation = self.get_object()
|
||||
|
||||
if escalation.status not in ['ACKNOWLEDGED', 'TRIGGERED']:
|
||||
return Response(
|
||||
{'error': 'Only acknowledged or triggered escalations can be resolved'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
escalation.status = 'RESOLVED'
|
||||
escalation.resolved_at = timezone.now()
|
||||
escalation.save()
|
||||
|
||||
return Response({'message': 'Escalation resolved'})
|
||||
|
||||
|
||||
class NotificationTemplateViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for Notification Template management"""
|
||||
|
||||
queryset = NotificationTemplate.objects.all()
|
||||
serializer_class = NotificationTemplateSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['template_type', 'channel_type', 'is_active', 'is_default']
|
||||
search_fields = ['name', 'subject_template']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
ordering = ['template_type', 'channel_type', 'name']
|
||||
Reference in New Issue
Block a user