Merge branch 'fix-15' into 'main'

 — Implement mail and gotify alerts. Fix #15 and #16

Closes #16 et #15

See merge request framasoft/framaspace/argos!13
This commit is contained in:
Luc Didry 2023-12-14 15:35:10 +00:00
commit a4e6799fd0
8 changed files with 184 additions and 12 deletions

View file

@ -441,7 +441,9 @@ disable=raw-checker-failed,
singleton-comparison, singleton-comparison,
missing-module-docstring, missing-module-docstring,
missing-class-docstring, missing-class-docstring,
missing-function-docstring missing-function-docstring,
too-many-arguments,
too-many-locals,
# Enable the message, report, category or checker with the given id(s). You can # Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option # either give multiple identifier separated by comma (,) or put this option

View file

@ -22,13 +22,13 @@ Internally, a HTTP API is exposed, and a job queue is used to distribute the che
- [X] donner un aperçu rapide de létat de la supervision. - [X] donner un aperçu rapide de létat de la supervision.
- [ ] Use background tasks for alerting (#23) - [ ] Use background tasks for alerting (#23)
- [ ] Delete outdated tasks from config (#19, !25) - [ ] Delete outdated tasks from config (#19, !25)
- [ ] Implement alerting tasks (#15, 16, !13) - [X] Implement alerting tasks (#15, 16, !13)
- [ ] Handles multiple alerting backends (email, sms, gotify) (!13) - [X] Handles multiple alerting backends (email, sms, gotify) (!13)
- [ ] add an "unknown" severity for check errors (!17) - [ ] add an "unknown" severity for check errors (!17)
- [ ] Add a command to generate new authentication token (#22) - [ ] Add a command to generate new authentication token (#22)
- [ ] Add a way to specify the severity of the alerts in the config - [ ] Add a way to specify the severity of the alerts in the config
- [ ] Allow passing a dict to check - [ ] Allow passing a dict to check
- [ ] Un flag de configuration permet dajouter automatiquement un job de vérification de redirection 301 de la version HTTP vers HTTPS - [ ] A configuration flag can automatically add a check of 301 redirection from HTTP to HTTPS
## License ## License

View file

@ -4,7 +4,15 @@ 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 +101,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: str = '127.0.0.1'
port: PositiveInt = 25
ssl: StrictBool = False
starttls: StrictBool = False
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"""
@ -102,11 +127,18 @@ class Alert(BaseModel):
unknown: List[str] unknown: List[str]
class GotifyUrl(BaseModel):
url: HttpUrl
tokens: List[str]
class General(BaseModel): class General(BaseModel):
"""Frequency for the checks and alerts""" """Frequency for the checks and alerts"""
frequency: int frequency: int
alerts: Alert alerts: Alert
mail: Optional[Mail] = None
gotify: Optional[List[GotifyUrl]] = None
@field_validator("frequency", mode="before") @field_validator("frequency", mode="before")
def parse_frequency(cls, value): def parse_frequency(cls, value):

View file

@ -1,9 +1,125 @@
import ssl
import smtplib
from typing import List
from urllib.parse import urlparse
import httpx
from argos.checks.base import Severity
from argos.logging import logger from argos.logging import logger
from argos.schemas.config import Config, Mail, GotifyUrl
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15 # XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
# XXX Implement gotify alerts https://framagit.org/framasoft/framaspace/argos/-/issues/16 # XXX Implement gotify alerts https://framagit.org/framasoft/framaspace/argos/-/issues/16
def handle_alert(config, result, task, severity): def handle_alert(config: Config, result, task, severity, old_severity, request):
"""Dispatch alert through configured alert channels""" """Dispatch alert through configured alert channels"""
msg = f"task={task.id}, status={result.status}, {severity=}"
logger.error("Alerting stub: %s", msg) if 'local' in getattr(config.general.alerts, severity):
logger.error("Alerting stub: task=%i, status=%s, severity=%s",
task.id,
result.status,
severity)
if config.general.mail is not None and \
'mail' in getattr(config.general.alerts, severity):
notify_by_mail(result, task, severity, old_severity, config.general.mail, request)
if config.general.gotify is not None and \
'gotify' in getattr(config.general.alerts, severity):
notify_with_gotify(result, task, severity, old_severity, config.general.gotify, request)
def notify_by_mail(result, task, severity: str, old_severity: str, config: Mail, request) -> None:
logger.debug('Will send mail notification')
msg = f"""\
URL: {task.url}
Check: {task.check}
Status: {severity}
Time: {result.submitted_at}
Previous status: {old_severity}
See results of task on {request.url_for('get_task_results', task_id=task.id)}
"""
mail = f"""\
Subject: [Argos] {urlparse(task.url).netloc}: status {severity}
{msg}"""
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 config.auth is not 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.mailfrom, address, mail)
def notify_with_gotify(
result,
task,
severity: str,
old_severity: str,
config: List[GotifyUrl],
request
) -> None:
logger.debug('Will send gotify notification')
headers = {'accept': 'application/json',
'content-type': 'application/json'}
priority = 9
icon = ''
if severity == Severity.OK:
priority = 1
icon = ''
elif severity == Severity.WARNING:
priority = 5
icon = '⚠️'
subject = f"{icon} {urlparse(task.url).netloc}: status {severity}"
msg = f"""\
URL: {task.url}
Check: {task.check}
Status: {severity}
Time: {result.submitted_at}
Previous status: {old_severity}
See results of task on {request.url_for('get_task_results', task_id=task.id)}
"""
payload = {'title': subject,
'message': msg,
'priority': priority}
for url in config:
logger.debug('Sending gotify message(s) to %s', url)
for token in url.tokens:
try:
res = httpx.post(f"{url.url}message",
params={'token': token},
headers=headers,
json=payload)
res.raise_for_status()
except httpx.RequestError as err:
logger.error('An error occurred while sending a message to %s with token %s',
err.request.url,
token)

View file

@ -44,19 +44,23 @@ 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()
handle_alert(config, result, task, severity) # Dont create an alert if the severity has not changed
if last_severity != severity:
# XXX Use a job queue or make it async
handle_alert(config, result, task, severity, last_severity, request)
db_results.append(result) db_results.append(result)
db.commit() db.commit()

View file

@ -12,7 +12,7 @@
{{ counts_dict['ok'] }} {{ counts_dict['ok'] }}
</article> </article>
<article> <article>
<header> ⚠️ Warning</header> <header>⚠️ Warning</header>
{{ counts_dict['warning'] }} {{ counts_dict['warning'] }}
</article> </article>
<article> <article>

View file

@ -10,6 +10,24 @@ general:
- local - local
unknown: unknown:
- local - local
# mail:
# mailfrom: no-reply@example.org
# host: 127.0.0.1
# port: 25
# ssl: False
# starttls: False
# auth:
# login: foo
# password: bar
# addresses:
# - foo@admin.example.org
# - bar@admin.example.org
# gotify:
# - url: https://example.org
# tokens:
# - foo
# - bar
service: service:
secrets: secrets:
# Secrets can be generated using `openssl rand -base64 32`. # Secrets can be generated using `openssl rand -base64 32`.

View file

@ -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",