Switched from Dockmon to Beszel
This commit is contained in:
1
dockmon/backend/models/__init__.py
Normal file
1
dockmon/backend/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Data models module for DockMon
|
||||
18
dockmon/backend/models/auth_models.py
Normal file
18
dockmon/backend/models/auth_models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Authentication Models for DockMon
|
||||
Pydantic models for authentication requests and responses
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Login request model with validation"""
|
||||
username: str = Field(..., min_length=1, max_length=50)
|
||||
password: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""Change password request model"""
|
||||
current_password: str = Field(..., min_length=1, max_length=100)
|
||||
new_password: str = Field(..., min_length=8, max_length=100) # Minimum 8 characters for security
|
||||
175
dockmon/backend/models/docker_models.py
Normal file
175
dockmon/backend/models/docker_models.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
Docker Models for DockMon
|
||||
Pydantic models for Docker hosts, containers, and configurations
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, validator, model_validator
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DockerHostConfig(BaseModel):
|
||||
"""Configuration for a Docker host"""
|
||||
name: str = Field(..., min_length=1, max_length=100, pattern=r'^[a-zA-Z0-9][a-zA-Z0-9 ._-]*$')
|
||||
url: str = Field(..., min_length=1, max_length=500)
|
||||
tls_cert: Optional[str] = Field(None, max_length=10000)
|
||||
tls_key: Optional[str] = Field(None, max_length=10000)
|
||||
tls_ca: Optional[str] = Field(None, max_length=10000)
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
"""Validate host name for security"""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Host name cannot be empty')
|
||||
# Prevent XSS and injection
|
||||
sanitized = re.sub(r'[<>"\']', '', v.strip())
|
||||
if len(sanitized) != len(v.strip()):
|
||||
raise ValueError('Host name contains invalid characters')
|
||||
return sanitized
|
||||
|
||||
@validator('url')
|
||||
def validate_url(cls, v):
|
||||
"""Validate Docker URL for security - prevent SSRF attacks"""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('URL cannot be empty')
|
||||
|
||||
v = v.strip()
|
||||
|
||||
# Only allow specific protocols
|
||||
allowed_protocols = ['tcp://', 'unix://', 'http://', 'https://']
|
||||
if not any(v.startswith(proto) for proto in allowed_protocols):
|
||||
raise ValueError('URL must use tcp://, unix://, http:// or https:// protocol')
|
||||
|
||||
# Block ONLY the most dangerous SSRF targets (cloud metadata & loopback)
|
||||
# Allow private networks (10.*, 172.16-31.*, 192.168.*) for legitimate Docker hosts
|
||||
extremely_dangerous_patterns = [
|
||||
r'169\.254\.169\.254', # AWS/GCP metadata (specific)
|
||||
r'169\.254\.', # Link-local range (broader)
|
||||
r'metadata\.google\.internal', # GCP metadata
|
||||
r'metadata\.goog', # GCP metadata alternative
|
||||
r'100\.100\.100\.200', # Alibaba Cloud metadata
|
||||
r'fd00:ec2::254', # AWS IPv6 metadata
|
||||
r'0\.0\.0\.0', # All interfaces binding
|
||||
r'::1', # IPv6 localhost
|
||||
r'localhost(?!\:|$)', # Localhost variations but allow localhost:port
|
||||
r'127\.0\.0\.(?!1$)', # 127.x.x.x but allow 127.0.0.1
|
||||
]
|
||||
|
||||
# Check for extremely dangerous metadata service targets
|
||||
for pattern in extremely_dangerous_patterns:
|
||||
if re.search(pattern, v, re.IGNORECASE):
|
||||
# Special handling for localhost - allow localhost:port but block bare localhost
|
||||
if 'localhost' in pattern.lower() and ':' in v:
|
||||
continue # Allow localhost:2376 etc
|
||||
raise ValueError('URL targets cloud metadata service or dangerous internal endpoint')
|
||||
|
||||
# Additional validation: warn about but allow private networks
|
||||
private_network_patterns = [
|
||||
r'10\.', # 10.0.0.0/8
|
||||
r'172\.(1[6-9]|2[0-9]|3[01])\.', # 172.16.0.0/12
|
||||
r'192\.168\.', # 192.168.0.0/16
|
||||
]
|
||||
|
||||
# Log private network usage for monitoring (but don't block)
|
||||
for pattern in private_network_patterns:
|
||||
if re.search(pattern, v, re.IGNORECASE):
|
||||
logger.info(f"Docker host configured on private network: {v[:50]}...")
|
||||
break
|
||||
|
||||
return v
|
||||
|
||||
@validator('tls_cert', 'tls_key', 'tls_ca')
|
||||
def validate_certificate(cls, v):
|
||||
"""Validate TLS certificate data"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
|
||||
# Basic PEM format validation with helpful error messages
|
||||
if '-----BEGIN' not in v and '-----END' not in v:
|
||||
raise ValueError('Certificate is incomplete. PEM certificates must start with "-----BEGIN" and end with "-----END". Please copy the entire certificate including both lines.')
|
||||
elif '-----BEGIN' not in v:
|
||||
raise ValueError('Certificate is missing the "-----BEGIN" header line. Make sure you copied the complete certificate starting from the "-----BEGIN" line.')
|
||||
elif '-----END' not in v:
|
||||
raise ValueError('Certificate is missing the "-----END" footer line. Make sure you copied the complete certificate including the "-----END" line.')
|
||||
|
||||
# Block potential code injection
|
||||
dangerous_patterns = ['<script', 'javascript:', 'data:', 'vbscript:', '<?php', '<%', '{{', '{%']
|
||||
v_lower = v.lower()
|
||||
if any(pattern in v_lower for pattern in dangerous_patterns):
|
||||
raise ValueError('Certificate contains potentially dangerous content')
|
||||
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_tls_complete(self):
|
||||
"""Ensure TLS configuration is complete when using TCP with certificates"""
|
||||
# Only validate for TCP connections
|
||||
if not self.url or not self.url.startswith('tcp://'):
|
||||
return self
|
||||
|
||||
# If any TLS field is provided, all three must be provided
|
||||
tls_fields_provided = [
|
||||
('Client Certificate', self.tls_cert),
|
||||
('Client Private Key', self.tls_key),
|
||||
('CA Certificate', self.tls_ca)
|
||||
]
|
||||
|
||||
provided_fields = [(name, val) for name, val in tls_fields_provided if val and val.strip()]
|
||||
|
||||
if provided_fields and len(provided_fields) < 3:
|
||||
# Some but not all fields provided
|
||||
missing = [name for name, val in tls_fields_provided if not val or not val.strip()]
|
||||
missing_str = ', '.join(missing)
|
||||
raise ValueError(
|
||||
f'Incomplete TLS configuration. For secure TCP connections, you must provide all three certificates. '
|
||||
f'Missing: {missing_str}. Either provide all three certificates or remove all of them.'
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class DockerHost(BaseModel):
|
||||
"""Docker host with connection status"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
name: str
|
||||
url: str
|
||||
status: str = "offline"
|
||||
security_status: Optional[str] = None # "secure", "insecure", "unknown"
|
||||
last_checked: datetime = Field(default_factory=datetime.now)
|
||||
container_count: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class Container(BaseModel):
|
||||
"""Container information"""
|
||||
id: str
|
||||
short_id: str
|
||||
name: str
|
||||
state: str
|
||||
status: str
|
||||
host_id: str
|
||||
host_name: str
|
||||
image: str
|
||||
created: str
|
||||
auto_restart: bool = False
|
||||
restart_attempts: int = 0
|
||||
# Stats from Go stats service
|
||||
cpu_percent: Optional[float] = None
|
||||
memory_usage: Optional[int] = None
|
||||
memory_limit: Optional[int] = None
|
||||
memory_percent: Optional[float] = None
|
||||
network_rx: Optional[int] = None
|
||||
network_tx: Optional[int] = None
|
||||
disk_read: Optional[int] = None
|
||||
disk_write: Optional[int] = None
|
||||
275
dockmon/backend/models/request_models.py
Normal file
275
dockmon/backend/models/request_models.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Request Models for DockMon API Endpoints
|
||||
Pydantic models for API request validation
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
class ContainerHostPair(BaseModel):
|
||||
"""Container and host pair for alert rules"""
|
||||
host_id: str = Field(..., max_length=50)
|
||||
container_name: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
|
||||
class AutoRestartRequest(BaseModel):
|
||||
"""Request model for toggling auto-restart"""
|
||||
host_id: str
|
||||
container_name: str
|
||||
enabled: bool
|
||||
|
||||
|
||||
class AlertRuleCreate(BaseModel):
|
||||
"""Request model for creating alert rules"""
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
containers: Optional[List[ContainerHostPair]] = Field(None, max_items=100)
|
||||
trigger_events: Optional[List[str]] = Field(None, max_items=20) # Docker events
|
||||
trigger_states: Optional[List[str]] = Field(None, max_items=10) # Docker states
|
||||
notification_channels: List[int] = Field(..., min_items=1, max_items=20)
|
||||
cooldown_minutes: int = Field(15, ge=1, le=1440) # 1 min to 24 hours
|
||||
enabled: bool = True
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
"""Validate rule name for security"""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Rule name cannot be empty')
|
||||
# Sanitize and prevent XSS
|
||||
sanitized = re.sub(r'[<>"\']', '', v.strip())
|
||||
if len(sanitized) != len(v.strip()):
|
||||
raise ValueError('Rule name contains invalid characters')
|
||||
return sanitized
|
||||
|
||||
@validator('trigger_events')
|
||||
def validate_trigger_events(cls, v):
|
||||
"""Validate Docker events"""
|
||||
if not v:
|
||||
return v # Events are optional
|
||||
|
||||
valid_events = {
|
||||
# Critical events
|
||||
'oom', 'die-nonzero', 'health_status:unhealthy',
|
||||
# Warning events
|
||||
'kill', 'die-zero', 'restart-loop', 'stuck-removing',
|
||||
# Info events
|
||||
'start', 'stop', 'create', 'destroy', 'pause', 'unpause',
|
||||
'health_status:healthy'
|
||||
}
|
||||
|
||||
invalid_events = [event for event in v if event not in valid_events]
|
||||
if invalid_events:
|
||||
raise ValueError(f'Invalid trigger events: {invalid_events}')
|
||||
|
||||
return v
|
||||
|
||||
@validator('trigger_states')
|
||||
def validate_trigger_states(cls, v):
|
||||
"""Validate container states"""
|
||||
if not v:
|
||||
return v # States are optional now
|
||||
|
||||
valid_states = {'created', 'restarting', 'running', 'removing', 'paused', 'exited', 'dead'}
|
||||
invalid_states = [state for state in v if state not in valid_states]
|
||||
if invalid_states:
|
||||
raise ValueError(f'Invalid trigger states: {invalid_states}')
|
||||
|
||||
return v
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Custom validation to ensure at least one trigger is specified"""
|
||||
super().__init__(**data)
|
||||
events = self.trigger_events or []
|
||||
states = self.trigger_states or []
|
||||
if not events and not states:
|
||||
raise ValueError('At least one trigger event or state is required')
|
||||
|
||||
@validator('notification_channels')
|
||||
def validate_notification_channels(cls, v):
|
||||
"""Validate notification channel IDs"""
|
||||
if not v:
|
||||
raise ValueError('At least one notification channel is required')
|
||||
|
||||
# Validate all are positive integers
|
||||
invalid_ids = [id for id in v if id <= 0]
|
||||
if invalid_ids:
|
||||
raise ValueError(f'Invalid notification channel IDs: {invalid_ids}')
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class AlertRuleUpdate(BaseModel):
|
||||
"""Request model for updating alert rules"""
|
||||
name: Optional[str] = None
|
||||
containers: Optional[List[ContainerHostPair]] = None
|
||||
trigger_events: Optional[List[str]] = None # Docker events
|
||||
trigger_states: Optional[List[str]] = None # Docker states
|
||||
notification_channels: Optional[List[int]] = None
|
||||
cooldown_minutes: Optional[int] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class NotificationChannelCreate(BaseModel):
|
||||
"""Request model for creating notification channels"""
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
type: str = Field(..., min_length=1, max_length=20)
|
||||
config: Dict[str, Any] = Field(..., min_items=1, max_items=10)
|
||||
enabled: bool = True
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
"""Validate channel name for security"""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Channel name cannot be empty')
|
||||
# Sanitize and prevent XSS
|
||||
sanitized = re.sub(r'[<>"\']', '', v.strip())
|
||||
if len(sanitized) != len(v.strip()):
|
||||
raise ValueError('Channel name contains invalid characters')
|
||||
return sanitized
|
||||
|
||||
@validator('type')
|
||||
def validate_type(cls, v):
|
||||
"""Validate notification type"""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Channel type cannot be empty')
|
||||
|
||||
valid_types = {'telegram', 'discord', 'pushover', 'slack', 'gotify', 'smtp'}
|
||||
v = v.strip().lower()
|
||||
|
||||
if v not in valid_types:
|
||||
raise ValueError(f'Invalid channel type. Must be one of: {valid_types}')
|
||||
|
||||
return v
|
||||
|
||||
@validator('config')
|
||||
def validate_config(cls, v, values):
|
||||
"""Validate channel configuration based on type"""
|
||||
if not v:
|
||||
raise ValueError('Configuration cannot be empty')
|
||||
|
||||
channel_type = values.get('type', '').lower()
|
||||
|
||||
# Validate configuration based on channel type
|
||||
if channel_type == 'telegram':
|
||||
required_keys = {'bot_token', 'chat_id'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'Telegram config must contain: {required_keys}')
|
||||
|
||||
# Validate bot token format
|
||||
bot_token = v.get('bot_token', '')
|
||||
if not re.match(r'^\d+:[A-Za-z0-9_-]+$', bot_token):
|
||||
raise ValueError('Invalid Telegram bot token format')
|
||||
|
||||
elif channel_type == 'discord':
|
||||
required_keys = {'webhook_url'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'Discord config must contain: {required_keys}')
|
||||
|
||||
# Validate Discord webhook URL
|
||||
webhook_url = v.get('webhook_url', '')
|
||||
if not webhook_url.startswith('https://discord.com/api/webhooks/'):
|
||||
raise ValueError('Invalid Discord webhook URL')
|
||||
|
||||
elif channel_type == 'slack':
|
||||
required_keys = {'webhook_url'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'Slack config must contain: {required_keys}')
|
||||
|
||||
# Validate Slack webhook URL
|
||||
webhook_url = v.get('webhook_url', '')
|
||||
if not (webhook_url.startswith('https://hooks.slack.com/services/') or
|
||||
webhook_url.startswith('https://hooks.slack.com/workflows/')):
|
||||
raise ValueError('Invalid Slack webhook URL')
|
||||
|
||||
elif channel_type == 'pushover':
|
||||
required_keys = {'app_token', 'user_key'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'Pushover config must contain: {required_keys}')
|
||||
|
||||
# Validate token formats
|
||||
app_token = v.get('app_token', '')
|
||||
user_key = v.get('user_key', '')
|
||||
if not re.match(r'^[a-z0-9]{30}$', app_token, re.IGNORECASE):
|
||||
raise ValueError('Invalid Pushover app token format')
|
||||
if not re.match(r'^[a-z0-9]{30}$', user_key, re.IGNORECASE):
|
||||
raise ValueError('Invalid Pushover user key format')
|
||||
|
||||
elif channel_type == 'gotify':
|
||||
required_keys = {'server_url', 'app_token'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'Gotify config must contain: {required_keys}')
|
||||
|
||||
# Validate server URL
|
||||
server_url = v.get('server_url', '')
|
||||
if not (server_url.startswith('http://') or server_url.startswith('https://')):
|
||||
raise ValueError('Gotify server URL must start with http:// or https://')
|
||||
|
||||
elif channel_type == 'smtp':
|
||||
required_keys = {'smtp_host', 'from_email', 'to_email'}
|
||||
if not all(key in v for key in required_keys):
|
||||
raise ValueError(f'SMTP config must contain: {required_keys}')
|
||||
|
||||
# Default port to 587 if not provided or empty
|
||||
if 'smtp_port' not in v or v['smtp_port'] == '' or v['smtp_port'] is None:
|
||||
v['smtp_port'] = 587
|
||||
|
||||
# Validate email format
|
||||
import re
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
from_email = v.get('from_email', '')
|
||||
to_email = v.get('to_email', '')
|
||||
if not re.match(email_pattern, from_email):
|
||||
raise ValueError('Invalid from_email format')
|
||||
if not re.match(email_pattern, to_email):
|
||||
raise ValueError('Invalid to_email format')
|
||||
|
||||
# Validate port
|
||||
try:
|
||||
port = int(v.get('smtp_port', 587))
|
||||
if port < 1 or port > 65535:
|
||||
raise ValueError('SMTP port must be between 1 and 65535')
|
||||
v['smtp_port'] = port # Ensure it's stored as int
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError('SMTP port must be a valid number')
|
||||
|
||||
# Validate all string values in config for security
|
||||
for key, value in v.items():
|
||||
if isinstance(value, str):
|
||||
# Prevent code injection in configuration values
|
||||
dangerous_patterns = ['<script', 'javascript:', 'data:', 'vbscript:', '<?php', '<%']
|
||||
value_lower = value.lower()
|
||||
if any(pattern in value_lower for pattern in dangerous_patterns):
|
||||
raise ValueError(f'Configuration value for {key} contains potentially dangerous content')
|
||||
|
||||
# Limit string length to prevent DoS
|
||||
if len(value) > 1000:
|
||||
raise ValueError(f'Configuration value for {key} is too long (max 1000 characters)')
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class NotificationChannelUpdate(BaseModel):
|
||||
"""Request model for updating notification channels"""
|
||||
name: Optional[str] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class EventLogFilter(BaseModel):
|
||||
"""Request model for filtering events"""
|
||||
category: Optional[str] = None
|
||||
event_type: Optional[str] = None
|
||||
severity: Optional[str] = None
|
||||
host_id: Optional[str] = None
|
||||
container_id: Optional[str] = None
|
||||
container_name: Optional[str] = None
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
correlation_id: Optional[str] = None
|
||||
search: Optional[str] = None
|
||||
limit: int = 100
|
||||
offset: int = 0
|
||||
85
dockmon/backend/models/settings_models.py
Normal file
85
dockmon/backend/models/settings_models.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Settings and Configuration Models for DockMon
|
||||
Pydantic models for global settings, alerts, and notifications
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
class GlobalSettings(BaseModel):
|
||||
"""Global monitoring settings"""
|
||||
max_retries: int = Field(3, ge=0, le=10) # 0-10 retries
|
||||
retry_delay: int = Field(30, ge=5, le=300) # 5 seconds to 5 minutes
|
||||
default_auto_restart: bool = False
|
||||
polling_interval: int = Field(2, ge=1, le=300) # 1 second to 5 minutes
|
||||
connection_timeout: int = Field(10, ge=1, le=60) # 1-60 seconds
|
||||
alert_template: Optional[str] = Field(None, max_length=2000) # Global notification template
|
||||
blackout_windows: Optional[List[dict]] = None # Blackout windows configuration
|
||||
timezone_offset: int = Field(0, ge=-720, le=720) # Timezone offset in minutes from UTC (-12h to +12h)
|
||||
show_host_stats: bool = Field(True) # Show host statistics graphs on dashboard
|
||||
show_container_stats: bool = Field(True) # Show container statistics on dashboard
|
||||
|
||||
@validator('max_retries')
|
||||
def validate_max_retries(cls, v):
|
||||
"""Validate retry count to prevent resource exhaustion"""
|
||||
if v < 0:
|
||||
raise ValueError('Max retries cannot be negative')
|
||||
if v > 10:
|
||||
raise ValueError('Max retries cannot exceed 10 to prevent resource exhaustion')
|
||||
return v
|
||||
|
||||
@validator('retry_delay')
|
||||
def validate_retry_delay(cls, v):
|
||||
"""Validate retry delay to prevent system overload"""
|
||||
if v < 5:
|
||||
raise ValueError('Retry delay must be at least 5 seconds')
|
||||
if v > 300:
|
||||
raise ValueError('Retry delay cannot exceed 300 seconds')
|
||||
return v
|
||||
|
||||
@validator('polling_interval')
|
||||
def validate_polling_interval(cls, v):
|
||||
"""Validate polling interval to prevent system overload"""
|
||||
if v < 1:
|
||||
raise ValueError('Polling interval must be at least 1 second')
|
||||
if v > 300:
|
||||
raise ValueError('Polling interval cannot exceed 300 seconds')
|
||||
return v
|
||||
|
||||
@validator('connection_timeout')
|
||||
def validate_connection_timeout(cls, v):
|
||||
"""Validate connection timeout"""
|
||||
if v < 1:
|
||||
raise ValueError('Connection timeout must be at least 1 second')
|
||||
if v > 60:
|
||||
raise ValueError('Connection timeout cannot exceed 60 seconds')
|
||||
return v
|
||||
|
||||
|
||||
class AlertRule(BaseModel):
|
||||
"""Alert rule configuration"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
name: str
|
||||
trigger_events: Optional[List[str]] = None
|
||||
trigger_states: Optional[List[str]] = None
|
||||
notification_channels: List[int]
|
||||
cooldown_minutes: int = 15
|
||||
enabled: bool = True
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
last_triggered: Optional[datetime] = None
|
||||
|
||||
|
||||
class NotificationSettings(BaseModel):
|
||||
"""Notification channel settings"""
|
||||
telegram_token: Optional[str] = None
|
||||
telegram_chat_id: Optional[str] = None
|
||||
discord_webhook: Optional[str] = None
|
||||
pushover_app_token: Optional[str] = None
|
||||
pushover_user_key: Optional[str] = None
|
||||
Reference in New Issue
Block a user