🔀 Merge branch 'develop'

This commit is contained in:
Luc Didry 2024-09-04 17:23:14 +02:00
commit 3a3c5852d0
No known key found for this signature in database
GPG key ID: EA868E12D0257E3C
7 changed files with 360 additions and 20 deletions

View file

@ -2,6 +2,11 @@
## [Unreleased] ## [Unreleased]
- 💄 — Improve email and gotify notifications
- ✨ — Add command to test gotify configuration
- ✨ — Add nagios command to use as a Nagios probe
- ✨ — Add Apprise as notification way (#50)
## 0.3.1 ## 0.3.1
Date: 2024-09-02 Date: 2024-09-02

View file

@ -607,12 +607,196 @@ async def test_mail(config, domain, severity):
notify_by_mail( notify_by_mail(
result, result,
task, task,
severity="SEVERITY", severity=severity,
old_severity="OLD SEVERITY", old_severity="OLD SEVERITY",
config=conf.general.mail, config=conf.general.mail,
request=_FalseRequest(), request=_FalseRequest(),
) )
@server.command()
@click.option(
"--config",
default="argos-config.yaml",
help="Path of the configuration file. "
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
envvar="ARGOS_YAML_FILE",
callback=validate_config_access,
)
@click.option("--domain", help="Domain for the notification", default="example.org")
@click.option("--severity", help="Severity", default="CRITICAL")
@coroutine
async def test_gotify(config, domain, severity):
"""Send a test gotify notification"""
os.environ["ARGOS_YAML_FILE"] = config
from datetime import datetime
from argos.logging import set_log_level
from argos.server.alerting import notify_with_gotify
from argos.server.main import read_config
from argos.server.models import Result, Task
conf = read_config(config)
if not conf.general.gotify:
click.echo("Gotify notifications are not configured, cannot test", err=True)
sysexit(1)
else:
now = datetime.now()
task = Task(
url=f"https://{domain}",
domain=domain,
check="body-contains",
expected="foo",
frequency=1,
selected_by="test",
selected_at=now,
)
result = Result(
submitted_at=now,
status="success",
context={"foo": "bar"},
task=task,
agent_id="test",
severity="ok",
)
class _FalseRequest:
def url_for(*args, **kwargs):
return "/url"
set_log_level("debug")
notify_with_gotify(
result,
task,
severity=severity,
old_severity="OLD SEVERITY",
config=conf.general.gotify,
request=_FalseRequest(),
)
@server.command()
@click.option(
"--config",
default="argos-config.yaml",
help="Path of the configuration file. "
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
envvar="ARGOS_YAML_FILE",
callback=validate_config_access,
)
@click.option("--domain", help="Domain for the notification", default="example.org")
@click.option("--severity", help="Severity", default="CRITICAL")
@click.option(
"--apprise-group", help="Apprise group for the notification", required=True
)
@coroutine
async def test_apprise(config, domain, severity, apprise_group):
"""Send a test apprise notification"""
os.environ["ARGOS_YAML_FILE"] = config
from datetime import datetime
from argos.logging import set_log_level
from argos.server.alerting import notify_with_apprise
from argos.server.main import read_config
from argos.server.models import Result, Task
conf = read_config(config)
if not conf.general.apprise:
click.echo("Apprise notifications are not configured, cannot test", err=True)
sysexit(1)
else:
now = datetime.now()
task = Task(
url=f"https://{domain}",
domain=domain,
check="body-contains",
expected="foo",
frequency=1,
selected_by="test",
selected_at=now,
)
result = Result(
submitted_at=now,
status="success",
context={"foo": "bar"},
task=task,
agent_id="test",
severity="ok",
)
class _FalseRequest:
def url_for(*args, **kwargs):
return "/url"
set_log_level("debug")
notify_with_apprise(
result,
task,
severity=severity,
old_severity="OLD SEVERITY",
group=conf.general.apprise[apprise_group],
request=_FalseRequest(),
)
@server.command(short_help="Nagios compatible severities report")
@click.option(
"--config",
default="argos-config.yaml",
help="Path of the configuration file. "
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
envvar="ARGOS_YAML_FILE",
callback=validate_config_access,
)
@coroutine
async def nagios(config):
"""Output a report of current severities suitable for Nagios
with a Nagios compatible exit code"""
os.environ["ARGOS_YAML_FILE"] = config
# The imports are made here otherwise the agent will need server configuration files.
from argos.server import queries
exit_nb = 0
db = await get_db()
severities = await queries.get_severity_counts(db)
if severities["warning"] != 0:
exit_nb = 1
if severities["critical"] != 0:
exit_nb = 2
if severities["unknown"] != 0:
exit_nb = 2
stats = (
f"ok={severities['ok']}; warning={severities['warning']}; "
f"critical={severities['critical']}; unknown={severities['unknown']};"
)
if exit_nb == 0:
print("OK — All sites are ok|{stats}")
elif exit_nb == 1:
print(f"WARNING — {severities['warning']} sites are in warning state|{stats}")
elif severities["critical"] == 0:
print(f"UNKNOWN — {severities['unknown']} sites are in unknown state|{stats}")
elif severities["unknown"] == 0:
print(
f"CRITICAL — {severities['critical']} sites are in critical state|{stats}"
)
else:
print(
f"CRITICAL/UNKNOWN — {severities['critical']} sites are in critical state "
f"and {severities['unknown']} sites are in unknown state|{stats}"
)
sysexit(exit_nb)
if __name__ == "__main__": if __name__ == "__main__":
cli() cli()

View file

@ -23,7 +23,10 @@ general:
frequency: "1m" frequency: "1m"
# Which way do you want to be warned when a check goes to that severity? # Which way do you want to be warned when a check goes to that severity?
# "local" emits a message in the server log # "local" emits a message in the server log
# Youll need to configure mail and gotify below to be able to use them here. # Youll need to configure mail, gotify or apprise below to be able to use
# them here.
# Use "apprise:john", "apprise:team" (with the quotes!) to use apprise
# notification groups.
alerts: alerts:
ok: ok:
- local - local
@ -58,6 +61,17 @@ general:
# tokens: # tokens:
# - foo # - foo
# - bar # - bar
# See https://github.com/caronc/apprise#productivity-based-notifications
# for apprises URLs syntax.
# You need to surround the URLs with quotes like in the examples below.
# Use "apprise:john", "apprise:team" (with the quotes!) in "alerts" settings.
# apprise:
# john:
# - "mastodon://access_key@hostname/@user"
# - "matrixs://token@hostname:port/?webhook=matrix"
# team:
# - "mmosts://user@hostname/authkey"
# - "nctalks://user:pass@host/RoomId1/RoomId2/RoomIdN"
service: service:
secrets: secrets:

View file

@ -168,6 +168,7 @@ class General(BaseModel):
alerts: Alert alerts: Alert
mail: Optional[Mail] = None mail: Optional[Mail] = None
gotify: Optional[List[GotifyUrl]] = None gotify: Optional[List[GotifyUrl]] = None
apprise: Optional[Dict[str, List[str]]] = None
@field_validator("frequency", mode="before") @field_validator("frequency", mode="before")
def parse_frequency(cls, value): def parse_frequency(cls, value):

View file

@ -4,14 +4,24 @@ import smtplib
from typing import List from typing import List
from urllib.parse import urlparse from urllib.parse import urlparse
import apprise
import httpx import httpx
from argos.checks.base import Severity from argos.checks.base import Severity
from argos.logging import logger from argos.logging import logger
from argos.schemas.config import Config, Mail, GotifyUrl from argos.schemas.config import Config, Mail, GotifyUrl
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
# XXX Implement gotify alerts https://framagit.org/framasoft/framaspace/argos/-/issues/16 def get_icon_from_severity(severity: str) -> str:
icon = ""
if severity == Severity.OK:
icon = ""
elif severity == Severity.WARNING:
icon = "⚠️"
elif severity == Severity.UNKNOWN:
icon = ""
return icon
def handle_alert(config: Config, result, task, severity, old_severity, request): def handle_alert(config: Config, result, task, severity, old_severity, request):
@ -39,12 +49,52 @@ def handle_alert(config: Config, result, task, severity, old_severity, request):
result, task, severity, old_severity, config.general.gotify, request result, task, severity, old_severity, config.general.gotify, request
) )
if config.general.apprise is not None:
for notif_way in getattr(config.general.alerts, severity):
if notif_way.startswith("apprise:"):
group = notif_way[8:]
notify_with_apprise(
result,
task,
severity,
old_severity,
config.general.apprise[group],
request,
)
def notify_with_apprise(
result, task, severity: str, old_severity: str, group: List[str], request
) -> None:
logger.debug("Will send apprise notification")
apobj = apprise.Apprise()
for channel in group:
apobj.add(channel)
icon = get_icon_from_severity(severity)
title = f"[Argos] {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 result on {request.url_for('get_result_view', result_id=result.id)}
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}#{result.id}
"""
apobj.notify(title=title, body=msg)
def notify_by_mail( def notify_by_mail(
result, task, severity: str, old_severity: str, config: Mail, request result, task, severity: str, old_severity: str, config: Mail, request
) -> None: ) -> None:
logger.debug("Will send mail notification") logger.debug("Will send mail notification")
icon = get_icon_from_severity(severity)
msg = f"""\ msg = f"""\
URL: {task.url} URL: {task.url}
Check: {task.check} Check: {task.check}
@ -58,7 +108,7 @@ See results of task on {request.url_for('get_task_results_view', task_id=task.id
""" """
mail = f"""\ mail = f"""\
Subject: [Argos] {urlparse(task.url).netloc}: status {severity} Subject: [Argos] {icon} {urlparse(task.url).netloc}: status {severity}
{msg}""" {msg}"""
@ -92,29 +142,35 @@ def notify_with_gotify(
logger.debug("Will send gotify notification") logger.debug("Will send gotify notification")
headers = {"accept": "application/json", "content-type": "application/json"} headers = {"accept": "application/json", "content-type": "application/json"}
icon = get_icon_from_severity(severity)
priority = 9 priority = 9
icon = ""
if severity == Severity.OK: if severity == Severity.OK:
priority = 1 priority = 1
icon = ""
elif severity == Severity.WARNING: elif severity == Severity.WARNING:
priority = 5 priority = 5
icon = "⚠️" elif severity == Severity.UNKNOWN:
priority = 5
subject = f"{icon} {urlparse(task.url).netloc}: status {severity}" subject = f"{icon} {urlparse(task.url).netloc}: status {severity}"
msg = f"""\ msg = f"""\
URL: {task.url} URL:    <{task.url}>\\
Check: {task.check} Check:  {task.check}\\
Status: {severity} Status: {severity}\\
Time: {result.submitted_at} Time:   {result.submitted_at}\\
Previous status: {old_severity} Previous status: {old_severity}\\
\\
See result on {request.url_for('get_result_view', result_id=result.id)} See result on <{request.url_for('get_result_view', result_id=result.id)}>\\
\\
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}#{result.id} See results of task on <{request.url_for('get_task_results_view', task_id=task.id)}#{result.id}>
""" """
extras = {
"client::display": {"contentType": "text/markdown"},
"client::notification": {
"click": {"url": request.url_for("get_result_view", result_id=result.id)}
},
}
payload = {"title": subject, "message": msg, "priority": priority} payload = {"title": subject, "message": msg, "priority": priority, "extras": extras}
for url in config: for url in config:
logger.debug("Sending gotify message(s) to %s", url) logger.debug("Sending gotify message(s) to %s", url)

View file

@ -83,8 +83,11 @@ Commands:
generate-config Output a self-documented example config file. generate-config Output a self-documented example config file.
generate-token Generate a token for agents generate-token Generate a token for agents
migrate Run database migrations migrate Run database migrations
nagios Nagios compatible severities report
reload-config Load or reload tasks configuration reload-config Load or reload tasks configuration
start Starts the server (use only for testing or development!) start Starts the server (use only for testing or development!)
test-apprise Send a test apprise notification
test-gotify Send a test gotify notification
test-mail Send a test email test-mail Send a test email
user User management user User management
watch-agents Watch agents (to run routinely) watch-agents Watch agents (to run routinely)
@ -467,9 +470,33 @@ Options:
<!--[[[end]]] <!--[[[end]]]
--> -->
#### Use as a nagios probe
You can directly use Argos to get an output and an exit code usable with Nagios.
<!--
.. [[[cog
help(["server", "nagios", "--help"])
.. ]]] -->
```man
Usage: argos server nagios [OPTIONS]
Output a report of current severities suitable for Nagios with a Nagios
compatible exit code
Options:
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment
variable is set, its value will be used instead.
--help Show this message and exit.
```
<!--[[[end]]]
-->
#### Test the email settings #### Test the email settings
You can verify that your mail settings are ok by sending a test email You can verify that your mail settings are ok by sending a test email.
<!-- <!--
.. [[[cog .. [[[cog
@ -491,3 +518,55 @@ Options:
<!--[[[end]]] <!--[[[end]]]
--> -->
#### Test the Gotify settings
You can verify that your Gotify settings are ok by sending a test notification.
<!--
.. [[[cog
help(["server", "test-gotify", "--help"])
.. ]]] -->
```man
Usage: argos server test-gotify [OPTIONS]
Send a test gotify notification
Options:
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE
environment variable is set, its value will be used instead.
--domain TEXT Domain for the notification
--severity TEXT Severity
--help Show this message and exit.
```
<!--[[[end]]]
-->
#### Test the Apprise settings
You can verify that your Apprise settings are ok by sending a test notification.
<!--
.. [[[cog
help(["server", "test-apprise", "--help"])
.. ]]] -->
```man
Usage: argos server test-apprise [OPTIONS]
Send a test apprise notification
Options:
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE
environment variable is set, its value will be used
instead.
--domain TEXT Domain for the notification
--severity TEXT Severity
--apprise-group TEXT Apprise group for the notification [required]
--help Show this message and exit.
```
<!--[[[end]]]
-->

View file

@ -22,11 +22,12 @@ classifiers = [
dependencies = [ dependencies = [
"alembic>=1.13.0,<1.14", "alembic>=1.13.0,<1.14",
"apprise>=1.9.0,<2",
"bcrypt>=4.1.3,<5", "bcrypt>=4.1.3,<5",
"click>=8.1,<9", "click>=8.1,<9",
"fastapi>=0.103,<0.104", "fastapi>=0.103,<0.104",
"fastapi-login>=1.10.0,<2", "fastapi-login>=1.10.0,<2",
"httpx>=0.25,<0.27.0", "httpx>=0.27.2,<1",
"Jinja2>=3.0,<4", "Jinja2>=3.0,<4",
"jsonpointer>=3.0,<4", "jsonpointer>=3.0,<4",
"passlib>=1.7.4,<2", "passlib>=1.7.4,<2",