mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
🔀 Merge branch 'develop'
This commit is contained in:
commit
3a3c5852d0
7 changed files with 360 additions and 20 deletions
|
@ -2,6 +2,11 @@
|
|||
|
||||
## [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
|
||||
|
||||
Date: 2024-09-02
|
||||
|
|
|
@ -607,12 +607,196 @@ async def test_mail(config, domain, severity):
|
|||
notify_by_mail(
|
||||
result,
|
||||
task,
|
||||
severity="SEVERITY",
|
||||
severity=severity,
|
||||
old_severity="OLD SEVERITY",
|
||||
config=conf.general.mail,
|
||||
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__":
|
||||
cli()
|
||||
|
|
|
@ -23,7 +23,10 @@ general:
|
|||
frequency: "1m"
|
||||
# Which way do you want to be warned when a check goes to that severity?
|
||||
# "local" emits a message in the server log
|
||||
# You’ll need to configure mail and gotify below to be able to use them here.
|
||||
# You’ll 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:
|
||||
ok:
|
||||
- local
|
||||
|
@ -58,6 +61,17 @@ general:
|
|||
# tokens:
|
||||
# - foo
|
||||
# - bar
|
||||
# See https://github.com/caronc/apprise#productivity-based-notifications
|
||||
# for apprise’s 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:
|
||||
secrets:
|
||||
|
|
|
@ -168,6 +168,7 @@ class General(BaseModel):
|
|||
alerts: Alert
|
||||
mail: Optional[Mail] = None
|
||||
gotify: Optional[List[GotifyUrl]] = None
|
||||
apprise: Optional[Dict[str, List[str]]] = None
|
||||
|
||||
@field_validator("frequency", mode="before")
|
||||
def parse_frequency(cls, value):
|
||||
|
|
|
@ -4,14 +4,24 @@ import smtplib
|
|||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import apprise
|
||||
import httpx
|
||||
|
||||
from argos.checks.base import Severity
|
||||
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 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):
|
||||
|
@ -39,12 +49,52 @@ def handle_alert(config: Config, result, task, severity, old_severity, 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(
|
||||
result, task, severity: str, old_severity: str, config: Mail, request
|
||||
) -> None:
|
||||
logger.debug("Will send mail notification")
|
||||
|
||||
icon = get_icon_from_severity(severity)
|
||||
msg = f"""\
|
||||
URL: {task.url}
|
||||
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"""\
|
||||
Subject: [Argos] {urlparse(task.url).netloc}: status {severity}
|
||||
Subject: [Argos] {icon} {urlparse(task.url).netloc}: status {severity}
|
||||
|
||||
{msg}"""
|
||||
|
||||
|
@ -92,29 +142,35 @@ def notify_with_gotify(
|
|||
logger.debug("Will send gotify notification")
|
||||
headers = {"accept": "application/json", "content-type": "application/json"}
|
||||
|
||||
icon = get_icon_from_severity(severity)
|
||||
priority = 9
|
||||
icon = "❌"
|
||||
if severity == Severity.OK:
|
||||
priority = 1
|
||||
icon = "✅"
|
||||
elif severity == Severity.WARNING:
|
||||
priority = 5
|
||||
icon = "⚠️"
|
||||
elif severity == Severity.UNKNOWN:
|
||||
priority = 5
|
||||
|
||||
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 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}
|
||||
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}>
|
||||
"""
|
||||
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:
|
||||
logger.debug("Sending gotify message(s) to %s", url)
|
||||
|
|
81
docs/cli.md
81
docs/cli.md
|
@ -83,8 +83,11 @@ Commands:
|
|||
generate-config Output a self-documented example config file.
|
||||
generate-token Generate a token for agents
|
||||
migrate Run database migrations
|
||||
nagios Nagios compatible severities report
|
||||
reload-config Load or reload tasks’ configuration
|
||||
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
|
||||
user User management
|
||||
watch-agents Watch agents (to run routinely)
|
||||
|
@ -467,9 +470,33 @@ Options:
|
|||
<!--[[[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
|
||||
|
||||
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
|
||||
|
@ -491,3 +518,55 @@ Options:
|
|||
|
||||
<!--[[[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]]]
|
||||
-->
|
||||
|
|
|
@ -22,11 +22,12 @@ classifiers = [
|
|||
|
||||
dependencies = [
|
||||
"alembic>=1.13.0,<1.14",
|
||||
"apprise>=1.9.0,<2",
|
||||
"bcrypt>=4.1.3,<5",
|
||||
"click>=8.1,<9",
|
||||
"fastapi>=0.103,<0.104",
|
||||
"fastapi-login>=1.10.0,<2",
|
||||
"httpx>=0.25,<0.27.0",
|
||||
"httpx>=0.27.2,<1",
|
||||
"Jinja2>=3.0,<4",
|
||||
"jsonpointer>=3.0,<4",
|
||||
"passlib>=1.7.4,<2",
|
||||
|
|
Loading…
Reference in a new issue