mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
✨ — Implement mail alerts. Fix #15
This commit is contained in:
parent
bb39e53845
commit
de1f0cc22a
4 changed files with 96 additions and 4 deletions
|
@ -4,7 +4,7 @@ For database models, see argos.server.models.
|
||||||
"""
|
"""
|
||||||
from typing import Dict, List, Literal, Optional, Tuple
|
from typing import Dict, List, Literal, Optional, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
|
from pydantic import BaseModel, ConfigDict, HttpUrl, StrictBool, EmailStr, PositiveInt, field_validator
|
||||||
from pydantic.functional_validators import BeforeValidator
|
from pydantic.functional_validators import BeforeValidator
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
@ -93,6 +93,23 @@ class Service(BaseModel):
|
||||||
secrets: List[str]
|
secrets: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class MailAuth(BaseModel):
|
||||||
|
"""Mail authentication configuration"""
|
||||||
|
login: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class Mail(BaseModel):
|
||||||
|
"""Mail configuration"""
|
||||||
|
mailfrom: EmailStr
|
||||||
|
host: Optional[str] = None
|
||||||
|
port: Optional[PositiveInt] = None
|
||||||
|
ssl: Optional[StrictBool] = None
|
||||||
|
starttls: Optional[StrictBool] = None
|
||||||
|
auth: Optional[MailAuth] = None
|
||||||
|
addresses: List[EmailStr]
|
||||||
|
|
||||||
|
|
||||||
class Alert(BaseModel):
|
class Alert(BaseModel):
|
||||||
"""List of way to handle alerts, by severity"""
|
"""List of way to handle alerts, by severity"""
|
||||||
|
|
||||||
|
@ -107,6 +124,7 @@ class General(BaseModel):
|
||||||
|
|
||||||
frequency: int
|
frequency: int
|
||||||
alerts: Alert
|
alerts: Alert
|
||||||
|
mail: Optional[Mail] = None
|
||||||
|
|
||||||
@field_validator("frequency", mode="before")
|
@field_validator("frequency", mode="before")
|
||||||
def parse_frequency(cls, value):
|
def parse_frequency(cls, value):
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import ssl
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from argos.logging import logger
|
from argos.logging import logger
|
||||||
|
|
||||||
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
|
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
|
||||||
|
@ -7,3 +12,69 @@ def handle_alert(config, result, task, severity):
|
||||||
"""Dispatch alert through configured alert channels"""
|
"""Dispatch alert through configured alert channels"""
|
||||||
msg = f"task={task.id}, status={result.status}, {severity=}"
|
msg = f"task={task.id}, status={result.status}, {severity=}"
|
||||||
logger.error("Alerting stub: %s", msg)
|
logger.error("Alerting stub: %s", msg)
|
||||||
|
|
||||||
|
if 'mail' in config and 'addresses' in config['mail']:
|
||||||
|
logger.debug('Notify of failure by mail')
|
||||||
|
|
||||||
|
msg = f"""\
|
||||||
|
Subject: Argos {urlparse(task.url).netloc} status {severity}
|
||||||
|
|
||||||
|
URL: {task.url}
|
||||||
|
Check: {task.check}
|
||||||
|
Status: {severity}
|
||||||
|
Time: {result.submitted_at}
|
||||||
|
"""
|
||||||
|
notify_by_mail(msg=msg, config=config['mail'])
|
||||||
|
|
||||||
|
|
||||||
|
def notify_by_mail(msg: str, config: dict) -> None:
|
||||||
|
"""Notify by mail
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
msg -- string, the mail to send (including subject)
|
||||||
|
config -- dict, configuration of mail system
|
||||||
|
"""
|
||||||
|
if 'mailfrom' not in config:
|
||||||
|
logger.error('No "mailfrom" address in mail config. '
|
||||||
|
'Not sending mail.')
|
||||||
|
return None
|
||||||
|
if 'host' not in config:
|
||||||
|
config['host'] = '127.0.0.1'
|
||||||
|
if 'port' not in config:
|
||||||
|
config['port'] = 25
|
||||||
|
if 'ssl' not in config:
|
||||||
|
config['ssl'] = False
|
||||||
|
if 'starttls' not in config:
|
||||||
|
config['starttls'] = False
|
||||||
|
if config['ssl']:
|
||||||
|
logger.debug('Mail notification: SSL')
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
smtp = smtplib.SMTP_SSL(host=config['host'],
|
||||||
|
port=config['port'],
|
||||||
|
context=context)
|
||||||
|
else:
|
||||||
|
smtp = smtplib.SMTP(host=config['host'], # type: ignore
|
||||||
|
port=config['port'])
|
||||||
|
if config['starttls']:
|
||||||
|
logger.debug('Mail notification: STARTTLS')
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
smtp.starttls(context=context)
|
||||||
|
|
||||||
|
if 'auth' in config:
|
||||||
|
if 'login' not in config['auth'] \
|
||||||
|
or 'password' not in config['auth']:
|
||||||
|
logger.warning('Mail credentials are incomplete. '
|
||||||
|
'No mail authentication can be done. '
|
||||||
|
'Not sending mail.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug('Mail notification: authentification')
|
||||||
|
smtp.login(config['auth']['login'],
|
||||||
|
config['auth']['password'])
|
||||||
|
|
||||||
|
for address in config['addresses']:
|
||||||
|
logger.debug('Sending mail to %s', address)
|
||||||
|
logger.debug(msg)
|
||||||
|
smtp.sendmail(config['from'], address, msg)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -44,18 +44,21 @@ async def create_results(
|
||||||
agent_id = agent_id or request.client.host
|
agent_id = agent_id or request.client.host
|
||||||
db_results = []
|
db_results = []
|
||||||
for agent_result in results:
|
for agent_result in results:
|
||||||
result = await queries.create_result(db, agent_result, agent_id)
|
|
||||||
# XXX Maybe offload this to a queue.
|
# XXX Maybe offload this to a queue.
|
||||||
# XXX Get all the tasks at once, to limit the queries on the db
|
# XXX Get all the tasks at once, to limit the queries on the db
|
||||||
task = await queries.get_task(db, agent_result.task_id)
|
task = await queries.get_task(db, agent_result.task_id)
|
||||||
if not task:
|
if not task:
|
||||||
logger.error("Unable to find task %i", agent_result.task_id)
|
logger.error("Unable to find task %i", agent_result.task_id)
|
||||||
else:
|
else:
|
||||||
|
last_severity = task.severity
|
||||||
|
result = await queries.create_result(db, agent_result, agent_id)
|
||||||
check = task.get_check()
|
check = task.get_check()
|
||||||
status, severity = await check.finalize(config, result, **result.context)
|
status, severity = await check.finalize(config, result, **result.context)
|
||||||
result.set_status(status, severity)
|
result.set_status(status, severity)
|
||||||
task.set_times_and_deselect()
|
task.set_times_and_deselect()
|
||||||
|
|
||||||
|
# Don’t create an alert if the severity has not changed
|
||||||
|
if last_severity != severity:
|
||||||
handle_alert(config, result, task, severity)
|
handle_alert(config, result, task, severity)
|
||||||
|
|
||||||
db_results.append(result)
|
db_results.append(result)
|
||||||
|
|
|
@ -20,7 +20,7 @@ dependencies = [
|
||||||
"click>=8.1,<9",
|
"click>=8.1,<9",
|
||||||
"fastapi>=0.103,<0.104",
|
"fastapi>=0.103,<0.104",
|
||||||
"httpx>=0.25,<1",
|
"httpx>=0.25,<1",
|
||||||
"pydantic>=2.4,<3",
|
"pydantic[email]>=2.4,<3",
|
||||||
"pyyaml>=6.0,<7",
|
"pyyaml>=6.0,<7",
|
||||||
"pyyaml-include>=1.3,<2",
|
"pyyaml-include>=1.3,<2",
|
||||||
"sqlalchemy[asyncio]>=2.0,<3",
|
"sqlalchemy[asyncio]>=2.0,<3",
|
||||||
|
|
Loading…
Reference in a new issue