From db54dd2cdd94691a4d0055c387ceba1a69f98bf9 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Wed, 12 Feb 2025 16:08:16 +0100 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20=E2=80=94=20Allow=20to=20cust?= =?UTF-8?q?omize=20agent=20User-Agent=20header=20(fix=20#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ argos/agent.py | 12 +++++++++--- argos/commands.py | 9 +++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba1a1c..fbcc3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- ✨ — Allow to customize agent User-Agent header (#78) + ## 0.7.4 Date: 2025-02-12 diff --git a/argos/agent.py b/argos/agent.py index 195ffdc..70689e5 100644 --- a/argos/agent.py +++ b/argos/agent.py @@ -37,11 +37,17 @@ def log_failure(retry_state): class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes """The Argos agent is responsible for running the checks and reporting the results.""" - def __init__(self, server: str, auth: str, max_tasks: int, wait_time: int): + def __init__( # pylint: disable-msg=too-many-positional-arguments + self, server: str, auth: str, max_tasks: int, wait_time: int, user_agent: str + ): self.server = server self.max_tasks = max_tasks self.wait_time = wait_time self.auth = auth + if user_agent == "": + self.ua = user_agent + else: + self.ua = f" - {user_agent}" self._http_client: httpx.AsyncClient | None = None self._http_client_v4: httpx.AsyncClient | None = None self._http_client_v6: httpx.AsyncClient | None = None @@ -53,13 +59,13 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes async def run(self): auth_header = { "Authorization": f"Bearer {self.auth}", - "User-Agent": f"Argos Panoptes agent {VERSION}", + "User-Agent": f"Argos Panoptes agent {VERSION}{self.ua}", } self._http_client = httpx.AsyncClient(headers=auth_header) ua_header = { "User-Agent": f"Argos Panoptes {VERSION} " - "(about: https://argos-monitoring.framasoft.org/)", + f"(about: https://argos-monitoring.framasoft.org/){self.ua}", } self._http_client_v4 = httpx.AsyncClient( headers=ua_header, diff --git a/argos/commands.py b/argos/commands.py index ac1309f..97c244e 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -92,7 +92,12 @@ def version(): default="INFO", type=click.Choice(logging.LOG_LEVELS, case_sensitive=False), ) -def agent(server_url, auth, max_tasks, wait_time, log_level): +@click.option( + "--user-agent", + default="", + help="A custom string to append to the User-Agent header", +) +def agent(server_url, auth, max_tasks, wait_time, log_level, user_agent): # pylint: disable-msg=too-many-positional-arguments """Get and run tasks for the provided server. Will wait for new tasks. Usage: argos agent https://argos.example.org "auth-token-here" @@ -108,7 +113,7 @@ def agent(server_url, auth, max_tasks, wait_time, log_level): from argos.logging import logger logger.setLevel(log_level) - agent_ = ArgosAgent(server_url, auth, max_tasks, wait_time) + agent_ = ArgosAgent(server_url, auth, max_tasks, wait_time, user_agent) asyncio.run(agent_.run()) From 73e7a8f4144d49e0ad5a3430789e607ae453d43f Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Wed, 12 Feb 2025 16:25:10 +0100 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Document=20how?= =?UTF-8?q?=20to=20add=20data=20to=20requests=20(fix=20#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + docs/checks.md | 42 ++++++++++++++++++++++++++++++++++++++++++ docs/cli.md | 2 ++ 3 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbcc3b6..991756a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - ✨ — Allow to customize agent User-Agent header (#78) +- 📝 — Document how to add data to requests (#77) ## 0.7.4 diff --git a/docs/checks.md b/docs/checks.md index 8e961e2..82ee77e 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -82,6 +82,48 @@ caption: argos-config.yaml - json-is: '{"foo": "bar", "baz": 42}' ``` +## Add data to requests + +If you want to specify query parameters, just put them in the path: + +```{code-block} yaml +websites: + - domain: "https://contact.example.org" + paths: + - path: "/index.php?action=show_messages" + method: "GET" +``` + +If you want, for example, to test a form and send some data to it: + +```{code-block} yaml +websites: + - domain: "https://contact.example.org" + paths: + - path: "/" + method: "POST" + request_data: + # These are the data sent to the server: title and msg + data: + title: "Hello my friend" + msg: "How are you today?" + # To send data as JSON (optional, default is false): + is_json: true +``` + +If you need to send some headers in the request: + +```{code-block} yaml +websites: + - domain: "https://contact.example.org" + paths: + - path: "/api/mail" + method: "PUT" + request_data: + headers: + Authorization: "Bearer foo-bar-baz" +``` + ## SSL certificate expiration Checks that the SSL certificate will not expire soon. You need to define the thresholds in the configuration, and set the `on-check` option to enable the check. diff --git a/docs/cli.md b/docs/cli.md index a9aac5e..e524a8c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -61,6 +61,8 @@ Options: --wait-time INTEGER Waiting time between two polls on the server (seconds) --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL] + --user-agent TEXT A custom string to append to the User-Agent + header --help Show this message and exit. ``` From c98cd9c0171f3a4184fc78d5aa0e1f152160aa4d Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 17 Feb 2025 10:46:01 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20=E2=80=94=20No=20need=20cron?= =?UTF-8?q?=20tasks=20for=20DB=20cleaning=20anymore=20(fix=20#74=20and=20#?= =?UTF-8?q?75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + argos/commands.py | 58 +--------------------------- argos/config-example.yaml | 21 +++++++--- argos/logging.py | 5 ++- argos/schemas/config.py | 22 +++++++++++ argos/server/main.py | 22 ++++++++++- docs/cli.md | 55 ++++++-------------------- docs/installation/getting-started.md | 12 ------ docs/installation/tl-dr.md | 3 +- pyproject.toml | 1 + tests/config.yaml | 29 +++++++++++++- tests/test_queries.py | 4 ++ 12 files changed, 109 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 991756a..deabd01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,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) ## 0.7.4 diff --git a/argos/commands.py b/argos/commands.py index 97c244e..3ac9555 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -140,73 +140,19 @@ def start(host, port, config, reload): uvicorn.run("argos.server:app", host=host, port=port, reload=reload) -def validate_max_lock_seconds(ctx, param, value): - if value <= 60: - raise click.BadParameter("Should be strictly higher than 60") - return value - - -def validate_max_results(ctx, param, value): +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( - "--max-results", - default=100, - help="Number of results per task to keep", - callback=validate_max_results, -) -@click.option( - "--max-lock-seconds", - default=100, - help=( - "The number of seconds after which a lock is " - "considered stale, must be higher than 60 " - "(the checks have a timeout value of 60 seconds)" - ), - callback=validate_max_lock_seconds, -) -@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. " - "Default value: argos-config.yaml and /etc/argos/config.yaml as fallback.", - envvar="ARGOS_YAML_FILE", - callback=validate_config_access, -) -@coroutine -async def cleandb(max_results, max_lock_seconds, config): - """Clean the database (to run routinely) - - \b - - Removes old results from the database. - - Removes locks from tasks that have been locked for too long. - """ - # 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() - removed = await queries.remove_old_results(db, max_results) - updated = await queries.release_old_locks(db, max_lock_seconds) - - click.echo(f"{removed} results removed") - click.echo(f"{updated} locks released") - - @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_max_results, + callback=validate_time_without_agent, ) @click.option( "--config", diff --git a/argos/config-example.yaml b/argos/config-example.yaml index 5508bff..2008a3e 100644 --- a/argos/config-example.yaml +++ b/argos/config-example.yaml @@ -81,6 +81,12 @@ general: # To disable the IPv6 check of domains: # ipv6: false + # Argos root path + # If not present, default value is "" + # Set it to /foo if you want to use argos at /foo/ instead of / + # on your web server + # root_path: "/foo" + # 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, gotify or apprise below to be able to use @@ -96,11 +102,6 @@ general: - local unknown: - local - # Argos root path - # If not present, default value is "" - # Set it to /foo if you want to use argos at /foo/ instead of / - # on your web server - # root_path: "/foo" # Mail configuration is quite straight-forward # mail: # mailfrom: no-reply@example.org @@ -144,6 +145,16 @@ ssl: - "1d": critical - "5d": warning +# Argos will do some cleaning in the background for you +# every 2 minutes and needs some configuration for that +cleaning: + # 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 + # It's also possible to define the checks in another file # with the include syntax: # diff --git a/argos/logging.py b/argos/logging.py index 5aba551..e67bfb9 100644 --- a/argos/logging.py +++ b/argos/logging.py @@ -14,9 +14,10 @@ logger = logging.getLogger(__name__) # XXX Does not work ? -def set_log_level(log_level): +def set_log_level(log_level: str, quiet: bool = False): level = getattr(logging, log_level.upper(), None) if not isinstance(level, int): raise ValueError(f"Invalid log level: {log_level}") logger.setLevel(level=level) - logger.info("Log level set to %s", log_level) + if not quiet: + logger.info("Log level set to %s", log_level) diff --git a/argos/schemas/config.py b/argos/schemas/config.py index 1baa7ed..68dd3ca 100644 --- a/argos/schemas/config.py +++ b/argos/schemas/config.py @@ -48,6 +48,27 @@ class SSL(BaseModel): thresholds: List[Annotated[Tuple[int, Severity], BeforeValidator(parse_threshold)]] +class Cleaning(BaseModel): + max_results: int + max_lock_seconds: int + + @field_validator("max_results", mode="before") + def parse_max_results(cls, value): + """Ensure that max_results is higher than 0""" + if value >= 1: + return value + + return 100 + + @field_validator("max_lock_seconds", mode="before") + def parse_max_lock_seconds(cls, value): + """Ensure that max_lock_seconds is higher or equal to agent’s requests timeout (60)""" + if value > 60: + return value + + return 100 + + class WebsiteCheck(BaseModel): key: str value: str | List[str] | Dict[str, str] @@ -264,4 +285,5 @@ class Config(BaseModel): general: General service: Service ssl: SSL + cleaning: Cleaning websites: List[Website] diff --git a/argos/server/main.py b/argos/server/main.py index 0543182..28dd21f 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -6,11 +6,12 @@ from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi_login import LoginManager +from fastapi_utils.tasks import repeat_every from pydantic import ValidationError from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker -from argos.logging import logger +from argos.logging import logger, set_log_level from argos.server import models, routes, queries from argos.server.exceptions import NotAuthenticatedException, auth_exception_handler from argos.server.settings import read_yaml_config @@ -126,8 +127,24 @@ def create_manager(cookie_secret: str) -> LoginManager: ) +@repeat_every(seconds=120, logger=logger) +async def cleanup() -> None: + set_log_level("info", quiet=True) + logger.info("Start DB cleanup 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 + ) + + logger.info("%i results removed", removed) + logger.info("%i locks released", updated) + + @asynccontextmanager -async def lifespan(appli): +async def lifespan(appli: FastAPI): """Server start and stop actions Setup database connection then close it at shutdown. @@ -142,6 +159,7 @@ async def lifespan(appli): "There is no tasks in the database. " 'Please launch the command "argos server reload-config"' ) + await cleanup() yield diff --git a/docs/cli.md b/docs/cli.md index e524a8c..ffccc03 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -84,7 +84,6 @@ Options: --help Show this message and exit. Commands: - cleandb Clean the database (to run routinely) generate-config Output a self-documented example config file. generate-token Generate a token for agents migrate Run database migrations @@ -152,60 +151,28 @@ Options: --> -### Server cleandb - - -```man -Usage: argos server cleandb [OPTIONS] - - Clean the database (to run routinely) - - - Removes old results from the database. - - Removes locks from tasks that have been locked for too long. - -Options: - --max-results INTEGER Number of results per task to keep - --max-lock-seconds INTEGER The number of seconds after which a lock is - considered stale, must be higher than 60 (the - checks have a timeout value of 60 seconds) - --config TEXT Path of the configuration file. If ARGOS_YAML_FILE - environment variable is set, its value will be - used instead. Default value: argos-config.yaml and - /etc/argos/config.yaml as fallback. - --help Show this message and exit. -``` - - - ### Server watch-agents ```man -Usage: argos server cleandb [OPTIONS] +Usage: argos server watch-agents [OPTIONS] - Clean the database (to run routinely) + Watch agents (to run routinely) - - Removes old results from the database. - - Removes locks from tasks that have been locked for too long. + Issues a warning if no agent has been seen by the server for a given time. Options: - --max-results INTEGER Number of results per task to keep - --max-lock-seconds INTEGER The number of seconds after which a lock is - considered stale, must be higher than 60 (the - checks have a timeout value of 60 seconds) - --config TEXT Path of the configuration file. If ARGOS_YAML_FILE - environment variable is set, its value will be - used instead. Default value: argos-config.yaml and - /etc/argos/config.yaml as fallback. - --help Show this message and exit. + --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 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