From 8d82f7f9d6886e964cee51913f12a37e78d4a76b Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 17 Feb 2025 14:54:25 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E2=80=94=20No=20need=20cron=20task?= =?UTF-8?q?s=20for=20agents=20watching=20(fix=20#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + argos/commands.py | 44 +----------- argos/config-example.yaml | 12 +++- argos/schemas/config.py | 14 +++- argos/server/alerting.py | 128 ++++++++++++++++++++++----------- argos/server/main.py | 25 ++++--- docs/cli.md | 28 -------- docs/developer/dependencies.md | 4 +- tests/config.yaml | 10 ++- tests/test_queries.py | 4 +- 10 files changed, 142 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deabd01..4be8ecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - ✨ — Allow to customize agent User-Agent header (#78) - 📝 — Document how to add data to requests (#77) - ✨ — No need cron tasks for DB cleaning anymore (#74 and #75) +- ✨ — No need cron tasks for agents watching (#76) ## 0.7.4 diff --git a/argos/commands.py b/argos/commands.py index 3ac9555..f33f4f5 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -140,47 +140,6 @@ def start(host, port, config, reload): uvicorn.run("argos.server:app", host=host, port=port, reload=reload) -def validate_time_without_agent(ctx, param, value): - if value <= 0: - raise click.BadParameter("Should be a positive integer") - return value - - -@server.command() -@click.option( - "--time-without-agent", - default=5, - help="Time without seeing an agent after which a warning will be issued, in minutes. " - "Default is 5 minutes.", - callback=validate_time_without_agent, -) -@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 watch_agents(time_without_agent, config): - """Watch agents (to run routinely) - - Issues a warning if no agent has been seen by the server for a given time. - """ - # It’s mandatory to do it before the imports - os.environ["ARGOS_YAML_FILE"] = config - - # The imports are made here otherwise the agent will need server configuration files. - from argos.server import queries - - db = await get_db() - agents = await queries.get_recent_agents_count(db, time_without_agent) - if agents == 0: - click.echo(f"No agent has been seen in the last {time_without_agent} minutes.") - sysexit(1) - - @server.command(short_help="Load or reload tasks’ configuration") @click.option( "--config", @@ -537,6 +496,7 @@ async def test_mail(config, domain, severity): check="body-contains", expected="foo", frequency=1, + ip_version=4, selected_by="test", selected_at=now, ) @@ -601,6 +561,7 @@ async def test_gotify(config, domain, severity): check="body-contains", expected="foo", frequency=1, + ip_version=4, selected_by="test", selected_at=now, ) @@ -668,6 +629,7 @@ async def test_apprise(config, domain, severity, apprise_group): check="body-contains", expected="foo", frequency=1, + ip_version=4, selected_by="test", selected_at=now, ) diff --git a/argos/config-example.yaml b/argos/config-example.yaml index 2008a3e..3b9141f 100644 --- a/argos/config-example.yaml +++ b/argos/config-example.yaml @@ -102,6 +102,10 @@ general: - local unknown: - local + # This alert is triggered when no Argos agent has been seen in a while + # See recurring_tasks.time_without_agent below + no_agent: + - local # Mail configuration is quite straight-forward # mail: # mailfrom: no-reply@example.org @@ -145,15 +149,19 @@ ssl: - "1d": critical - "5d": warning -# Argos will do some cleaning in the background for you +# Argos will execute some tasks in the background for you # every 2 minutes and needs some configuration for that -cleaning: +recurring_tasks: # Max number of results per tasks you want to keep # Minimum value is 1, default is 100 max_results: 100 # Max number of seconds a task can be locked # Minimum value is 61, default is 100 max_lock_seconds: 100 + # Max number of minutes without seing an agent + # before sending an alert + # Minimum value is 1, default is 5 + time_without_agent: 5 # It's also possible to define the checks in another file # with the include syntax: diff --git a/argos/schemas/config.py b/argos/schemas/config.py index 68dd3ca..26d0198 100644 --- a/argos/schemas/config.py +++ b/argos/schemas/config.py @@ -48,9 +48,10 @@ class SSL(BaseModel): thresholds: List[Annotated[Tuple[int, Severity], BeforeValidator(parse_threshold)]] -class Cleaning(BaseModel): +class RecurringTasks(BaseModel): max_results: int max_lock_seconds: int + time_without_agent: int @field_validator("max_results", mode="before") def parse_max_results(cls, value): @@ -68,6 +69,14 @@ class Cleaning(BaseModel): return 100 + @field_validator("time_without_agent", mode="before") + def parse_time_without_agent(cls, value): + """Ensure that time_without_agent is at least one minute""" + if value >= 1: + return value + + return 5 + class WebsiteCheck(BaseModel): key: str @@ -211,6 +220,7 @@ class Alert(BaseModel): warning: List[str] critical: List[str] unknown: List[str] + no_agent: List[str] class GotifyUrl(BaseModel): @@ -285,5 +295,5 @@ class Config(BaseModel): general: General service: Service ssl: SSL - cleaning: Cleaning + recurring_tasks: RecurringTasks websites: List[Website] diff --git a/argos/server/alerting.py b/argos/server/alerting.py index d533ed4..8cec99a 100644 --- a/argos/server/alerting.py +++ b/argos/server/alerting.py @@ -74,6 +74,91 @@ def get_icon_from_severity(severity: str) -> str: return icon +def send_mail(mail: EmailMessage, config: Mail): + """Send message by mail""" + + 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(mail.get_body()) + smtp.send_message(mail, to_addrs=address) + + +def send_gotify_msg(config, payload): + """Send message with gotify""" + headers = {"accept": "application/json", "content-type": "application/json"} + + for url in config: + logger.debug("Sending gotify message(s) to %s", url.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, + ) + + +def no_agent_alert(config: Config): + """Alert""" + msg = "You should check what’s going on with your Argos agents." + twa = config.recurring_tasks.time_without_agent + if twa > 1: + subject = f"No agent has been seen within the last {twa} minutes" + else: + subject = "No agent has been seen within the last minute" + + if "local" in config.general.alerts.no_agent: + logger.error(subject) + + if config.general.mail is not None and "mail" in config.general.alerts.no_agent: + mail = EmailMessage() + mail["Subject"] = f"[Argos] {subject}" + mail["From"] = config.general.mail.mailfrom + mail.set_content(msg) + send_mail(mail, config.general.mail) + + if config.general.gotify is not None and "gotify" in config.general.alerts.no_agent: + priority = 9 + payload = {"title": subject, "message": msg, "priority": priority} + send_gotify_msg(config.general.gotify, payload) + + if config.general.apprise is not None: + for notif_way in config.general.alerts.no_agent: + if notif_way.startswith("apprise:"): + group = notif_way[8:] + apobj = apprise.Apprise() + for channel in config.general.apprise[group]: + apobj.add(channel) + + apobj.notify(title=subject, body=msg) + + def handle_alert(config: Config, result, task, severity, old_severity, request): # pylint: disable-msg=too-many-positional-arguments """Dispatch alert through configured alert channels""" @@ -163,36 +248,13 @@ See results of task on {request.url_for('get_task_results_view', task_id=task.id ] = f"[Argos] {icon} {urlparse(task.url).netloc} (IPv{task.ip_version}): status {severity}" mail["From"] = config.mailfrom mail.set_content(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.send_message(mail, to_addrs=address) + send_mail(mail, config) def notify_with_gotify( # pylint: disable-msg=too-many-positional-arguments 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"} icon = get_icon_from_severity(severity) priority = 9 @@ -228,20 +290,4 @@ See results of task on <{request.url_for('get_task_results_view', task_id=task.i payload = {"title": subject, "message": msg, "priority": priority, "extras": extras} - for url in config: - logger.debug("Sending gotify message(s) to %s", url.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, - ) + send_gotify_msg(config, payload) diff --git a/argos/server/main.py b/argos/server/main.py index 28dd21f..a19a65e 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -13,6 +13,7 @@ from sqlalchemy.orm import sessionmaker from argos.logging import logger, set_log_level from argos.server import models, routes, queries +from argos.server.alerting import no_agent_alert from argos.server.exceptions import NotAuthenticatedException, auth_exception_handler from argos.server.settings import read_yaml_config @@ -128,20 +129,24 @@ def create_manager(cookie_secret: str) -> LoginManager: @repeat_every(seconds=120, logger=logger) -async def cleanup() -> None: +async def recurring_tasks() -> None: + """Recurring DB cleanup and watch-agents tasks""" set_log_level("info", quiet=True) - logger.info("Start DB cleanup tasks.") + logger.info("Start background recurring tasks") with app.state.SessionLocal() as db: - removed = await queries.remove_old_results( - db, app.state.config.cleaning.max_results - ) - updated = await queries.release_old_locks( - db, app.state.config.cleaning.max_lock_seconds - ) - + config = app.state.config.recurring_tasks + removed = await queries.remove_old_results(db, config.max_results) logger.info("%i results removed", removed) + + updated = await queries.release_old_locks(db, config.max_lock_seconds) logger.info("%i locks released", updated) + agents = await queries.get_recent_agents_count(db, config.time_without_agent) + if agents == 0: + no_agent_alert(app.state.config) + + logger.info("Background recurring tasks ended") + @asynccontextmanager async def lifespan(appli: FastAPI): @@ -159,7 +164,7 @@ async def lifespan(appli: FastAPI): "There is no tasks in the database. " 'Please launch the command "argos server reload-config"' ) - await cleanup() + await recurring_tasks() yield diff --git a/docs/cli.md b/docs/cli.md index ffccc03..55d9d36 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -94,7 +94,6 @@ Commands: test-gotify Send a test gotify notification test-mail Send a test email user User management - watch-agents Watch agents (to run routinely) ``` -### Server watch-agents - - - -```man -Usage: argos server watch-agents [OPTIONS] - - Watch agents (to run routinely) - - Issues a warning if no agent has been seen by the server for a given time. - -Options: - --time-without-agent INTEGER Time without seeing an agent after which a - warning will be issued, in minutes. Default is 5 - minutes. - --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. -``` - - - ### Server reload-config